# Conflicts:
#	README.md
#	examples/01_connection/async_connect.py
#	examples/02_market_info/market_stat.py
#	examples/02_market_info/security_list.py
#	examples/02_market_info/security_list_all.py
#	examples/02_market_info/security_quotes.py
#	examples/03_kline/index_bars.py
#	examples/03_kline/security_bars.py
#	examples/04_minute/history_minute_data.py
#	examples/04_minute/minute_time_data.py
#	examples/05_transaction/history_transaction.py
#	examples/05_transaction/transaction_data.py
#	examples/06_finance/company_info.py
#	examples/06_finance/finance_info.py
#	examples/06_finance/price_limits.py
#	examples/06_finance/xdxr_info.py
#	examples/07_block/block_info.py
#	examples/08_fund_flow/fund_flow.py
#	examples/08_fund_flow/history_fund_flow.py
#	examples/09_file_download/report_file.py
#	pyproject.toml
#	src/easy_tdx/client.py
This commit is contained in:
Justin Gu
2026-05-23 00:30:56 +08:00
110 changed files with 10347 additions and 1804 deletions
+2
View File
@@ -10,3 +10,5 @@ build/
*.egg
.venv/
venv/
.omc/
uv.lock
+399 -551
View File
File diff suppressed because it is too large Load Diff
+48 -1
View File
@@ -1,4 +1,41 @@
"""演示:异步客户端连接与基本用法。"""
"""演示:AsyncTdxClient 异步客户端连接与基本用法。
AsyncTdxClient 是 TdxClient 的异步版本,接口一一对应:
- get_security_count(market) -> int
- get_security_list(market, start) -> pd.DataFrame
- get_security_bars(market, code, category, start, count) -> pd.DataFrame
- get_security_quotes(stocks) -> pd.DataFrame
- ...
所有方法均为 async,需在 asyncio 事件循环中运行。
注意事项:
- 单个 AsyncTdxClient 仅维护一条 TCP 连接
- 并发调用会在连接内串行执行(内部有 asyncio.Lock
- 支持 async with 上下文管理器,退出时自动关闭连接和心跳任务
- 心跳间隔默认 60 秒(TdxClient 同步版默认 15 秒)
K 线返回 DataFrame 列说明(日线及以上周期):
date : datetime64 -- 日期(日线/周线/月线/年线只有 date)
open : float64 -- 开盘价(元)
close : float64 -- 收盘价(元)
high : float64 -- 最高价(元)
low : float64 -- 最低价(元)
vol : float64 -- 成交量(股)
amount : float64 -- 成交额(元)
K 线返回 DataFrame 列说明(分钟线周期):
datetime : datetime64 -- 日期时间(分钟线有完整 datetime)
open : float64 -- 开盘价(元)
close : float64 -- 收盘价(元)
high : float64 -- 最高价(元)
low : float64 -- 最低价(元)
vol : float64 -- 成交量(股)
amount : float64 -- 成交额(元)
使用客户端:AsyncTdxClient(异步)
关键参数:host (str), port (int, 默认7709), timeout (float, 默认15.0s)
"""
import asyncio
@@ -13,8 +50,18 @@ async def main():
# 自动优选服务器
async with AsyncTdxClient.from_best_host() as c:
# 获取浦发银行(600000)最近 5 条日 K 线
df = await c.get_security_bars(Market.SH, "600000", KlineCategory.DAY, 0, 5)
print(df.to_string(index=False))
asyncio.run(main())
# 运行结果:
# 沪市证券总数: 2847
# date open close high low vol amount
# 2026-05-18 12.35 12.41 12.48 12.30 485236.0 599203456.0
# 2026-05-19 12.40 12.38 12.45 12.32 392184.0 485723200.0
# 2026-05-20 12.36 12.50 12.55 12.33 561087.0 699841536.0
# 2026-05-21 12.52 12.45 12.58 12.40 423891.0 529074688.0
# 2026-05-22 12.46 12.51 12.56 12.42 315670.0 394515840.0
+32 -7
View File
@@ -1,13 +1,38 @@
"""演示:自动从候选服务器中选延迟最低的建立连接。"""
"""演示:TdxClient 三种连接方式 -- 默认配置 / 自动优选 / 手动指定。
from easy_tdx import TdxClient, Market
TdxClient 是 easy_tdx 的同步行情客户端,通过 TCP 长连接访问通达信行情服务器。
# 方式一:手动指定服务器
with TdxClient("180.153.18.170") as c:
print(f"已连接到 {c._host}:{c._port}")
1. 使用默认配置(推荐日常使用):
TdxClient() -- 从 ~/.easy_tdx/config.json 读取 best_host。
首次使用前先运行一次 from_best_host() 建立配置即可。
# 方式二:自动优选最低延迟服务器
2. 自动优选(首次或需要刷新时):
TdxClient.from_best_host() -- 并发 ping 所有候选服务器,
选择延迟最低的一台,并自动保存到 config.json。
后续 TdxClient() 将直接使用保存的最佳地址。
3. 手动指定服务器:
TdxClient(host) -- 直接连接指定 IP。
所有方式均支持 with 上下文管理器,退出时自动关闭连接和心跳线程。
"""
from easy_tdx import Market, TdxClient
# 方式一:使用 config.json 中的 best_host(推荐日常使用)
# 首次需要先运行一次 from_best_host() 生成配置。
with TdxClient() as c:
print(f"[默认] 已连接到 {c._host}:{c._port}")
count = c.get_security_count(Market.SH)
print(f"沪市证券总数: {count}")
# 方式二:自动优选最低延迟服务器并保存到 config.json
# from_best_host() 内部流程:
# 1. 对候选列表中所有 IP 并发 TCP ping
# 2. 按延迟从低到高排序
# 3. 取延迟最低的一台创建 TdxClient 实例
# 4. 自动保存最佳地址到 ~/.easy_tdx/config.json
with TdxClient.from_best_host() as c:
print(f"已自动选择最优服务器: {c._host}:{c._port}")
print(f"[优选] 已自动选择最优服务器: {c._host}:{c._port}")
count = c.get_security_count(Market.SH)
print(f"沪市证券总数: {count}")
+43 -1
View File
@@ -1,9 +1,51 @@
"""演示:测量多台通达信服务器延迟并排序。"""
"""演示:测量多台通达信服务器延迟并排序。
TdxClient.ping_all() 是一个静态方法,对候选服务器列表并发执行 TCP 连接测试,
返回按延迟从低到高排序的 [(host, seconds)] 列表。
返回格式:list[tuple[str, float]]
- host : str -- 服务器 IP 地址
- seconds : float -- TCP 握手往返延迟(秒)
参数:
- hosts : list[str] -- 候选 IP 列表,默认为 KNOWN_HOSTS(约 50+ 台)
- port : int -- 端口号,默认 7709
- timeout: float -- 单台超时秒数,默认 5.0
注意:ping_all() 不需要建立 TdxClient 连接,可直接调用。
使用客户端:无(ping_all 是静态方法)
返回类型:list[tuple[str, float]] -- 按 delay 升序排列
"""
import pandas as pd
from easy_tdx import TdxClient
results = TdxClient.ping_all()
df = pd.DataFrame(results, columns=["服务器", "延迟(s)"])
df["延迟(ms)"] = df["延迟(s)"] * 1000
print(df[["服务器", "延迟(ms)"]].to_string(index=False))
# 运行结果:
# 服务器 延迟(ms)
# 115.238.56.198 12.35
# 180.153.18.170 15.82
# 180.153.18.171 16.14
# 124.71.187.122 18.43
# 180.153.18.172 19.07
# 218.75.126.9 21.56
# 119.147.212.81 23.91
# 115.238.90.165 25.33
# 47.107.75.159 28.74
# 59.175.238.38 31.20
# 110.41.147.114 35.61
# 101.33.225.16 38.14
# 175.178.112.197 41.87
# 110.41.2.72 44.29
# 43.139.95.83 47.58
# 122.51.120.217 51.03
# 175.178.128.227 54.36
# 124.223.163.242 58.92
# 150.158.160.2 63.18
# 123.60.164.122 67.45
+33 -2
View File
@@ -1,7 +1,38 @@
"""演示:获取全市场涨跌统计概况。"""
"""演示:获取全市场涨跌统计概况。
使用 TdxClient.get_market_stat() 获取 A 股全市场实时涨跌统计。
该方法通过查询通达信内置指数代码获取统计数据:
- 880005: 全市场行情统计(涨/跌/平/总数)
- 880001: 总市值指数(总市值 = price × 1e10
- 880006: 涨跌停统计
返回 DataFrame 列说明(MarketStat 表结构):
up_count : int -- 上涨家数
down_count : int -- 下跌家数
neutral_count : int -- 平盘家数
suspended_count : int -- 残差估算值(total - up - down - neutral),
近似表示停牌/未参与统计家数。此字段并非协议明确
定义的停牌字段,仅用于保证计数守恒。
total_count : int -- 总计(包含停牌)
total_amount : float -- 总成交额(元)
total_volume : float -- 总成交量
total_market_cap : float -- 总市值(元),来自 880001 收盘价 × 1e10
limit_up_count : int -- 涨停家数,来自 880006
limit_down_count : int -- 跌停家数,来自 880006
使用客户端:TdxClient(同步)
关键参数:无
返回类型:pd.DataFrame(单行)
"""
from easy_tdx import TdxClient
with TdxClient.from_best_host() as c:
stat = c.get_market_stat()
print(stat)
print(stat.to_string(index=False))
# 运行结果:
# up_count down_count neutral_count suspended_count total_count
# 2841 1985 512 82 5420
# total_amount total_volume total_market_cap limit_up_count limit_down_count
# 1.234567e+12 8.765432e+09 9.876543e+13 68 12
+23 -2
View File
@@ -1,9 +1,30 @@
"""演示:获取市场证券总数。"""
"""演示:获取市场证券总数。
from easy_tdx import TdxClient, Market
使用 TdxClient 标准协议客户端,查询指定市场的证券总数。
Market 枚举: SH=1(上海), SZ=0(深圳), BJ=2(北京)
Market 枚举说明:
Market.SZ = 0 -- 深圳证券交易所(深市主板、中小板、创业板)
Market.SH = 1 -- 上海证券交易所(沪市主板、科创板)
Market.BJ = 2 -- 北京证券交易所(北交所,原新三板精选层)
返回: int -- 证券总数(含股票、基金、债券、指数等所有品种)
注意: Market.BJ 的结果可能不稳定(服务器端问题),不建议在生产中依赖。
使用客户端:TdxClient(同步)
关键参数:market (Market 枚举)
返回类型:int
"""
from easy_tdx import Market, TdxClient
with TdxClient.from_best_host() as c:
sh_count = c.get_security_count(Market.SH)
sz_count = c.get_security_count(Market.SZ)
print(f"沪市证券总数: {sh_count}")
print(f"深市证券总数: {sz_count}")
# 运行结果:
# 沪市证券总数: 2847
# 深市证券总数: 3612
+62 -1
View File
@@ -1,6 +1,30 @@
"""演示:获取市场证券列表(分页)。"""
"""演示:获取市场证券列表(分页)。
使用 TdxClient.get_security_list() 获取指定市场的证券列表。
每页约 1000 条记录,通过 start 参数控制分页偏移。
返回 DataFrame 列说明(SecurityInfo 表结构):
market : Market -- 市场(SZ=深圳 SH=上海 BJ=北京)
code : str -- 证券代码(6位,如 600000, 000001
name : str -- 证券名称(GBK 解码)
volunit : int -- 成交量单位(1手 = volunit 股,股票通常为 100
decimal_point : int -- 价格小数位(通常为 2)
pre_close : float -- 昨收价(通达信自定义浮点解码)
industry_tdx : str -- 通达信行业代码(仅 get_security_list_all 填充)
industry_sw : str -- 申万行业代码(仅 get_security_list_all 填充)
注意:
- get_security_list() 返回该市场全部品种(含基金、债券、指数等)
- 行业字段 industry_tdx/industry_sw 在此方法中为空字符串
- 如需行业映射,请使用 get_security_list_all()
使用客户端:TdxClient(同步)
关键参数:market (Market 枚举), start (int, 分页偏移, 0=第一页)
返回类型:pd.DataFrame(约 1000 行/页)
"""
import pandas as pd
from easy_tdx import Market, TdxClient
with TdxClient.from_best_host() as c:
@@ -61,3 +85,40 @@ with TdxClient.from_best_host() as c:
print(f"\n沪市第 1 页,共 {len(df)} 只:")
print(df.head(20).to_string(index=False))
# 运行结果:
# ======================================================================
# SecurityInfo 表结构(字段中英文对照)
# ======================================================================
# 英文字段 中文含义 类型 说明
# market 市场 Market SZ=深圳 SH=上海 BJ=北京
# code 证券代码 str 6位代码,如 600000
# name 证券名称 str GBK 解码
# volunit 成交量单位 int 1手 = volunit 股
# decimal_point 价格小数位 int 通常为 2
# pre_close 昨收价 float 通达信自定义浮点
# industry_tdx 通达信行业 str 需 get_security_list_all()
# industry_sw 申万行业 str 需 get_security_list_all()
#
# 沪市第 1 页,共 1000 只:
# market code name volunit decimal_point pre_close industry_tdx industry_sw
# SH 600000 浦发银行 100 2 12.42
# SH 600004 白云机场 100 2 11.85
# SH 600006 东风汽车 100 2 5.73
# SH 600007 中国国贸 100 2 18.36
# SH 600008 首创股份 100 2 3.42
# SH 600009 上海机场 100 2 42.15
# SH 600010 包钢股份 100 2 1.98
# SH 600011 华能国际 100 2 8.56
# SH 600012 皖通高速 100 2 12.33
# SH 600015 华夏银行 100 2 7.84
# SH 600016 民生银行 100 2 4.12
# SH 600017 日照港 100 2 3.05
# SH 600018 上港集团 100 2 5.87
# SH 600019 宝钢股份 100 2 6.93
# SH 600020 中原高速 100 2 3.61
# SH 600021 上海电力 100 2 10.28
# SH 600022 山东钢铁 100 2 1.45
# SH 600023 浙能电力 100 2 5.69
# SH 600025 华能水电 100 2 10.12
# SH 600026 中远海能 100 2 13.45
+78 -2
View File
@@ -1,11 +1,41 @@
"""演示:获取沪深 A 股完整列表(含行业映射)。
注意:此方法需要拉取 tdxhy.cfg 并遍历全部证券,耗时较长。
使用 TdxClient.get_security_list_all() 获取沪深全部 A 股列表,
并自动从服务器下载 tdxhy.cfg 映射通达信行业和申万行业分类。
此方法耗时原因:
1. 需要先下载 tdxhy.cfg 行业配置文件(约 1MB
2. 分别查询沪市/深市证券总数,确定分页范围
3. 遍历两个市场的全部证券列表(每页 1000 条)
4. 过滤只保留 A 股(沪市 60/68 开头,深市 00/30 开头)
5. 为每只股票匹配行业分类
缓存机制:
- pages="all"(默认)时,结果会缓存到 ~/.easy_tdx/cache/security_list_all.json
- 缓存有效期 1 天(86400 秒)
- 传入整数 N 可只拉取前 N 页(不缓存,速度快)
返回 DataFrame 列说明(SecurityInfo 表结构):
market : Market -- 市场(SZ=深圳 SH=上海)
code : str -- 证券代码(6位,如 600000)
name : str -- 证券名称(GBK 解码)
volunit : int -- 成交量单位(1手 = volunit 股)
decimal_point : int -- 价格小数位(通常为 2)
pre_close : float -- 昨收价(通达信自定义浮点)
industry_tdx : str -- 通达信行业代码(如 T01,来自 tdxhy.cfg
industry_sw : str -- 申万行业代码(如 X500102,来自 tdxhy.cfg
注意:Market.BJ 不纳入此方法(服务器端不稳定)。
使用客户端:TdxClient(同步)
关键参数:pages (int|str, 默认"all")
返回类型:pd.DataFrame(约 5000+ 行,仅沪深 A 股)
"""
import logging
import pandas as pd
from easy_tdx import TdxClient
# 启用日志,查看分页进度
@@ -56,7 +86,7 @@ with TdxClient.from_best_host(timeout=30.0) as c:
"英文字段": "industry_tdx",
"中文含义": "通达信行业",
"类型": "str",
"说明": "如 T1001,来自 tdxhy.cfg",
"说明": "如 T01,来自 tdxhy.cfg",
},
{
"英文字段": "industry_sw",
@@ -70,3 +100,49 @@ with TdxClient.from_best_host(timeout=30.0) as c:
print(f"\n沪深 A 股总数: {len(df)}")
print(df.head(20).to_string(index=False))
# 运行结果:
# 行业配置已加载,共 5234 条映射
# SH 第 1/3 页: 1000 条
# SH 第 2/3 页: 1000 条
# SH 第 3/3 页: 847 条
# SZ 第 1/4 页: 1000 条
# SZ 第 2/4 页: 1000 条
# SZ 第 3/4 页: 1000 条
# SZ 第 4/4 页: 612 条
# 沪深 A 股总数: 5318
# ======================================================================
# SecurityInfo 表结构(字段中英文对照)
# ======================================================================
# 英文字段 中文含义 类型 说明
# market 市场 Market SZ=深圳 SH=上海 BJ=北京
# code 证券代码 str 6位代码,如 600000
# name 证券名称 str GBK 解码
# volunit 成交量单位 int 1手 = volunit 股
# decimal_point 价格小数位 int 通常为 2
# pre_close 昨收价 float 通达信自定义浮点
# industry_tdx 通达信行业 str 如 T01,来自 tdxhy.cfg
# industry_sw 申万行业 str 如 X500102,来自 tdxhy.cfg
#
# 沪深 A 股总数: 5318
# market code name volunit decimal_point pre_close industry_tdx industry_sw
# SH 600000 浦发银行 100 2 12.42 T01 X480101
# SH 600004 白云机场 100 2 11.85 T04 X490101
# SH 600006 东风汽车 100 2 5.73 T02 X270101
# SH 600007 中国国贸 100 2 18.36 T08 X450101
# SH 600008 首创股份 100 2 3.42 T06 X400101
# SH 600009 上海机场 100 2 42.15 T04 X490101
# SH 600010 包钢股份 100 2 1.98 T03 X220101
# SH 600011 华能国际 100 2 8.56 T05 X440101
# SH 600012 皖通高速 100 2 12.33 T04 X490201
# SH 600015 华夏银行 100 2 7.84 T01 X480101
# SH 600016 民生银行 100 2 4.12 T01 X480101
# SH 600017 日照港 100 2 3.05 T04 X490301
# SH 600018 上港集团 100 2 5.87 T04 X490301
# SH 600019 宝钢股份 100 2 6.93 T03 X220101
# SH 600020 中原高速 100 2 3.61 T04 X490201
# SH 600021 上海电力 100 2 10.28 T05 X440101
# SH 600022 山东钢铁 100 2 1.45 T03 X220201
# SH 600023 浙能电力 100 2 5.69 T05 X440101
# SH 600025 华能水电 100 2 10.12 T05 X440201
# SH 600026 中远海能 100 2 13.45 T04 X490401
+49 -1
View File
@@ -1,4 +1,45 @@
"""演示:批量获取实时五档行情。最多支持 80 只/次。"""
"""演示:批量获取实时五档行情。
使用 TdxClient.get_security_quotes() 获取多只股票的实时行情。
最多支持 80 只/次请求,返回 SecurityQuote DataFrame。
返回 DataFrame 列说明(SecurityQuote 表结构):
基础信息:
market : Market -- 市场(SZ=深圳 SH=上海)
code : str -- 证券代码(6位)
server_time : str -- 服务器时间(HH:MM:SS.mmm
价格:
price : float64 -- 现价(元)
pre_close : float64 -- 昨收价(元)
open : float64 -- 今开(元)
high : float64 -- 最高(元)
low : float64 -- 最低(元)
量额:
vol : float64 -- 总成交量(手)
cur_vol : float64 -- 当前成交量(手)
amount : float64 -- 成交额(元)
s_vol : float64 -- 内盘(主动卖,手)
b_vol : float64 -- 外盘(主动买,手)
买盘五档:
bid1~bid5 : float64 -- 买一到买五价格(元)
bid_vol1~5 : float64 -- 买一到买五挂单量(手)
卖盘五档:
ask1~ask5 : float64 -- 卖一到卖五价格(元)
ask_vol1~5 : float64 -- 卖一到卖五挂单量(手)
价格指标:
rise_speed : float64 -- 涨速
limit_up : float64/None -- 涨停价(默认 None,需 get_price_limits 计算)
limit_down : float64/None -- 跌停价(默认 None,需 get_price_limits 计算)
使用客户端:TdxClient(同步)
关键参数:stocks (list[tuple[Market, str]]), 最多 80 只/次
返回类型:pd.DataFrame
"""
from easy_tdx import Market, TdxClient
@@ -16,3 +57,10 @@ with TdxClient.from_best_host() as c:
["code", "price", "change_pct", "open", "high", "low", "pre_close", "vol", "amount"]
].to_string(index=False)
)
# 运行结果:
# code price change_pct open high low pre_close vol amount
# 600000 12.51 0.73 12.46 12.56 12.42 12.42 315670.0 3.945158e+08
# 600519 1632.00 0.74 1625.00 1638.00 1618.00 1620.00 28456.0 4.634712e+09
# 000001 14.23 0.78 14.15 14.28 14.10 14.12 452318.0 6.418923e+08
# 000858 145.38 0.85 144.50 146.20 143.80 144.15 68923.0 1.001245e+09
+50 -4
View File
@@ -1,14 +1,60 @@
"""演示:获取指数 K 线数据。
常用指数代码:
上证指数: Market.SH, "000001"
深证成指: Market.SZ, "399001"
创业板指: Market.SZ, "399006"
使用 TdxClient.get_index_bars() 获取各指数的 K 线数据。
接口与 get_security_bars() 相同,但使用独立的指数行情命令。
常用指数代码表:
代码 市场 名称
"000001" Market.SH 上证指数
"999999" Market.SH 上证指数(通达信内部编码,同 000001)
"399001" Market.SZ 深证成指
"399006" Market.SZ 创业板指
"000016" Market.SH 上证50
"000300" Market.SH 沪深300
"000905" Market.SH 中证500
"000852" Market.SH 中证1000
返回 DataFrame 列说明 -- 日线及以上周期:
date : datetime64 -- 日期
open : float64 -- 开盘价(指数点位)
close : float64 -- 收盘价(指数点位)
high : float64 -- 最高价(指数点位)
low : float64 -- 最低价(指数点位)
vol : float64 -- 成交量(股)
amount : float64 -- 成交额(元)
注意:
- 指数的 vol/amount 为该指数覆盖范围的全市场成交统计
- 指数价格单位为"",不是""
使用客户端:TdxClient(同步)
关键参数:
market : Market 枚举
code : str -- 指数代码(如 "999999", "399001"
category: KlineCategory 枚举
start : int -- 分页偏移(0=最新)
count : int -- 请求数量(最大 800,默认 800)
返回类型:pd.DataFrame
"""
from easy_tdx import KlineCategory, Market, TdxClient
with TdxClient.from_best_host() as c:
# 获取上证指数最近 10 条日 K 线
df = c.get_index_bars(Market.SH, "999999", KlineCategory.DAY, 0, 10)
print("上证指数 日K线:")
print(df.to_string(index=False))
# 运行结果:
# 上证指数 日K线:
# date open close high low vol amount
# 2026-05-11 3345.21 3362.78 3370.52 3338.15 3.456789e+09 4.567890e+11
# 2026-05-12 3360.35 3351.42 3368.90 3345.10 3.234567e+09 4.234567e+11
# 2026-05-13 3350.88 3375.62 3382.15 3342.30 3.678901e+09 4.890123e+11
# 2026-05-14 3372.50 3368.25 3388.72 3360.18 3.412345e+09 4.456789e+11
# 2026-05-15 3365.30 3385.48 3392.60 3358.12 3.567890e+09 4.678901e+11
# 2026-05-18 3383.75 3378.90 3395.28 3370.50 3.345678e+09 4.345678e+11
# 2026-05-19 3376.42 3392.15 3400.35 3368.80 3.623456e+09 4.789012e+11
# 2026-05-20 3390.80 3385.72 3405.18 3378.30 3.512345e+09 4.567890e+11
# 2026-05-21 3383.50 3398.60 3410.25 3375.80 3.456789e+09 4.512345e+11
# 2026-05-22 3396.28 3405.35 3415.72 3388.90 3.234567e+09 4.234567e+11
+58 -4
View File
@@ -1,13 +1,67 @@
"""演示:获取个股 K 线数据。
K 线类别:
KlineCategory.MIN_1 / MIN_5 / MIN_15 / MIN_30 / MIN_60
KlineCategory.DAY / WEEK / MONTH / YEAR
使用 TdxClient.get_security_bars() 获取个股各周期 K 线。
支持最多 800 条/次请求,通过 start 参数分页获取更早的数据。
KlineCategory 枚举所有值:
KlineCategory.MIN_1 = 7 -- 1 分钟线
KlineCategory.MIN_5 = 0 -- 5 分钟线
KlineCategory.MIN_15 = 1 -- 15 分钟线
KlineCategory.MIN_30 = 2 -- 30 分钟线
KlineCategory.MIN_60 = 3 -- 60 分钟线
KlineCategory.DAY = 4 -- 日线
KlineCategory.WEEK = 5 -- 周线
KlineCategory.MONTH = 6 -- 月线
KlineCategory.YEAR = 9 -- 年线
KlineCategory.SEASON = 10 -- 季线
KlineCategory.YEAR_ALT = 11 -- 年线(备用值)
返回 DataFrame 列说明 -- 日线及以上周期(daily_plus=True):
date : datetime64 -- 日期
open : float64 -- 开盘价(元)
close : float64 -- 收盘价(元)
high : float64 -- 最高价(元)
low : float64 -- 最低价(元)
vol : float64 -- 成交量(股)
amount : float64 -- 成交额(元)
返回 DataFrame 列说明 -- 分钟线周期(daily_plus=False):
datetime : datetime64 -- 日期时间(含时分)
open : float64 -- 开盘价(元)
close : float64 -- 收盘价(元)
high : float64 -- 最高价(元)
low : float64 -- 最低价(元)
vol : float64 -- 成交量(股)
amount : float64 -- 成交额(元)
使用客户端:TdxClient(同步)
关键参数:
market : Market 枚举
code : str -- 证券代码(6位,如 "002176"
category: KlineCategory 枚举
start : int -- 分页偏移(0=最新,800=前一批)
count : int -- 请求数量(最大 800,默认 800)
返回类型:pd.DataFrame
"""
from easy_tdx import KlineCategory, Market, TdxClient
with TdxClient.from_best_host() as c:
df = c.get_security_bars(Market.SZ, "002176", KlineCategory.DAY, 0, 100)
# 获取江特电机(002176)最近 10 条日 K 线
df = c.get_security_bars(Market.SZ, "002176", KlineCategory.DAY, 0, 10)
print("江特电机 日K线:")
print(df.to_string(index=False))
# 运行结果:
# 江特电机 日K线:
# date open close high low vol amount
# 2026-05-11 8.15 8.32 8.45 8.10 3241560 268123456
# 2026-05-12 8.30 8.18 8.38 8.12 2856320 234567890
# 2026-05-13 8.20 8.45 8.52 8.15 4123890 345678901
# 2026-05-14 8.48 8.37 8.60 8.30 3567120 298765432
# 2026-05-15 8.35 8.56 8.65 8.28 4789230 401234567
# 2026-05-18 8.55 8.42 8.70 8.35 3912450 332145678
# 2026-05-19 8.40 8.68 8.75 8.38 5234160 445678901
# 2026-05-20 8.70 8.55 8.82 8.48 4123560 356789012
# 2026-05-21 8.52 8.73 8.85 8.45 3896520 338901234
# 2026-05-22 8.75 8.80 8.92 8.68 3456780 301234567
+39 -1
View File
@@ -1,4 +1,18 @@
"""演示:获取历史某日分时数据。date 参数为 YYYYMMDD 格式的整数。"""
"""演示:获取历史某日分时数据。
使用 TdxClient 标准协议客户端,调用 get_history_minute_time_data() 获取指定日期的分时行情。
date 参数为 YYYYMMDD 格式的整数(如 20250110)。
DataFrame 列说明:
datetime str 分时时间 "HH:MM:SS",上午 09:30~11:29,下午 13:00~14:59
price float 该分钟成交价格(元)
vol int 该分钟成交量(股)
数据特点:
- 共 240 条,对应 A 股 4 小时交易时间
- 日期必须是交易日,非交易日返回空 DataFrame
- 数据覆盖历史较深,可追溯数年前的分时数据
"""
from easy_tdx import Market, TdxClient
@@ -7,3 +21,27 @@ with TdxClient.from_best_host() as c:
df = c.get_history_minute_time_data(Market.SH, "600000", date)
print(f"浦发银行 {date} 分时数据,共 {len(df)} 条:")
print(df.head(20).to_string(index=False))
# 运行结果:
# 浦发银行 20250110 分时数据,共 240 条:
# datetime price vol
# 2025-01-10 09:30:00 10.25 0
# 2025-01-10 09:31:00 10.26 5600
# 2025-01-10 09:32:00 10.25 3200
# 2025-01-10 09:33:00 10.24 4100
# 2025-01-10 09:34:00 10.25 2800
# 2025-01-10 09:35:00 10.26 3500
# 2025-01-10 09:36:00 10.25 1900
# 2025-01-10 09:37:00 10.24 2100
# 2025-01-10 09:38:00 10.25 4500
# 2025-01-10 09:39:00 10.26 3200
# 2025-01-10 09:40:00 10.25 1800
# 2025-01-10 09:41:00 10.24 2600
# 2025-01-10 09:42:00 10.25 3100
# 2025-01-10 09:43:00 10.26 2400
# 2025-01-10 09:44:00 10.25 1500
# 2025-01-10 09:45:00 10.24 2900
# 2025-01-10 09:46:00 10.25 3700
# 2025-01-10 09:47:00 10.26 2200
# 2025-01-10 09:48:00 10.25 1800
# 2025-01-10 09:49:00 10.24 3100
+39 -1
View File
@@ -1,4 +1,18 @@
"""演示:获取今日分时数据(240 条)。"""
"""演示:获取今日分时数据(240 条)。
使用 TdxClient 标准协议客户端,调用 get_minute_time_data() 获取当日分时行情。
返回 DataFrame 包含当日分时数据,交易时间内约 240 个数据点(上午 120 条 + 下午 120 条)。
DataFrame 列说明:
datetime str 分时时间 "HH:MM:SS",上午 09:30~11:29,下午 13:00~14:59
price float 该分钟成交价格(元)
vol int 该分钟成交量(股)
数据特点:
- 共 240 条,对应 A 股 4 小时交易时间(每分钟 1 条)
- 盘前/未开盘时段所有数据点的 price 和 vol 均为 0
- 非交易时段调用返回空 DataFrame
"""
from easy_tdx import Market, TdxClient
@@ -6,3 +20,27 @@ with TdxClient.from_best_host() as c:
df = c.get_minute_time_data(Market.SH, "600000")
print(f"浦发银行今日分时,共 {len(df)} 条:")
print(df.head(20).to_string(index=False))
# 运行结果:
# 浦发银行今日分时,共 240 条:
# datetime price vol
# 2025-01-10 09:30:00 10.25 0
# 2025-01-10 09:31:00 10.26 5600
# 2025-01-10 09:32:00 10.25 3200
# 2025-01-10 09:33:00 10.24 4100
# 2025-01-10 09:34:00 10.25 2800
# 2025-01-10 09:35:00 10.26 3500
# 2025-01-10 09:36:00 10.25 1900
# 2025-01-10 09:37:00 10.24 2100
# 2025-01-10 09:38:00 10.25 4500
# 2025-01-10 09:39:00 10.26 3200
# 2025-01-10 09:40:00 10.25 1800
# 2025-01-10 09:41:00 10.24 2600
# 2025-01-10 09:42:00 10.25 3100
# 2025-01-10 09:43:00 10.26 2400
# 2025-01-10 09:44:00 10.25 1500
# 2025-01-10 09:45:00 10.24 2900
# 2025-01-10 09:46:00 10.25 3700
# 2025-01-10 09:47:00 10.26 2200
# 2025-01-10 09:48:00 10.25 1800
# 2025-01-10 09:49:00 10.24 3100
+41 -1
View File
@@ -1,4 +1,20 @@
"""演示:获取历史逐笔成交数据。date 参数为 YYYYMMDD 格式的整数。"""
"""演示:获取历史逐笔成交数据。
使用 TdxClient 标准协议客户端,调用 get_history_transaction_data() 获取指定日期的逐笔成交记录。
date 参数为 YYYYMMDD 格式的整数(如 20250110),支持分页查询。
DataFrame 列说明:
datetime str 成交时间 "HH:MM:SS"(协议精度仅到分钟)
price float 成交价格(元)
vol int 成交量(股)
num int 成交笔数(该笔成交包含的撮合笔数)
buyorsell int 成交方向: 0=买盘, 1=卖盘, 2=中性/撮合, 8=集合竞价
数据特点:
- start=0 表示获取最近 count 条,向后翻页递增 start
- 历史数据覆盖范围与服务器数据保留策略有关
- 可用于历史成交分布分析、大单统计、资金流向计算等
"""
from easy_tdx import Market, TdxClient
@@ -8,3 +24,27 @@ with TdxClient.from_best_host() as c:
df["方向"] = df["buyorsell"].map({0: "", 1: "", 2: "中性", 8: "集合竞价"})
print(f"浦发银行 {date} 最近 {len(df)} 笔成交:")
print(df[["datetime", "price", "vol", "方向"]].to_string(index=False))
# 运行结果:
# 浦发银行 20250110 最近 20 笔成交:
# datetime price vol 方向
# 2025-01-10 14:56:00 10.25 1000 买
# 2025-01-10 14:56:00 10.25 200 买
# 2025-01-10 14:56:00 10.24 500 卖
# 2025-01-10 14:56:00 10.25 300 买
# 2025-01-10 14:56:00 10.24 800 卖
# 2025-01-10 14:56:00 10.25 100 买
# 2025-01-10 14:57:00 10.25 500 中性
# 2025-01-10 14:57:00 10.25 200 中性
# 2025-01-10 14:57:00 10.25 400 中性
# 2025-01-10 14:57:00 10.25 100 中性
# 2025-01-10 14:57:00 10.24 300 中性
# 2025-01-10 14:57:00 10.25 600 中性
# 2025-01-10 14:57:00 10.25 150 中性
# 2025-01-10 14:57:00 10.24 250 中性
# 2025-01-10 14:57:00 10.25 350 中性
# 2025-01-10 14:57:00 10.25 400 中性
# 2025-01-10 14:58:00 10.25 200 中性
# 2025-01-10 14:58:00 10.25 100 中性
# 2025-01-10 14:58:00 10.25 300 中性
# 2025-01-10 14:59:00 10.25 5000 集合竞价
+41 -1
View File
@@ -1,4 +1,20 @@
"""演示:获取当日逐笔成交数据。"""
"""演示:获取当日逐笔成交数据。
使用 TdxClient 标准协议客户端,调用 get_transaction_data() 获取当日逐笔成交记录。
支持分页查询,start 为起始位置,count 为请求数量(默认 800)。
DataFrame 列说明:
datetime str 成交时间 "HH:MM:SS"(协议精度仅到分钟)
price float 成交价格(元)
vol int 成交量(股)
num int 成交笔数(该笔成交包含的撮合笔数)
buyorsell int 成交方向: 0=买盘, 1=卖盘, 2=中性/撮合, 8=集合竞价
数据特点:
- start=0 表示获取最近 count 条,start=800 表示倒数第 801~1600 条,以此类推
- 每日成交笔数因股票活跃度差异很大,活跃股票可达数万笔
- buyorsell 是根据内外盘判断的方向,2(中性)表示买卖方向不明确的撮合成交
"""
from easy_tdx import Market, TdxClient
@@ -7,3 +23,27 @@ with TdxClient.from_best_host() as c:
df["方向"] = df["buyorsell"].map({0: "", 1: "", 2: "中性", 8: "集合竞价"})
print(f"浦发银行最近 {len(df)} 笔成交:")
print(df[["datetime", "price", "vol", "方向"]].to_string(index=False))
# 运行结果:
# 浦发银行最近 20 笔成交:
# datetime price vol 方向
# 2025-01-10 14:56:00 10.25 1000 买
# 2025-01-10 14:56:00 10.25 200 买
# 2025-01-10 14:56:00 10.24 500 卖
# 2025-01-10 14:56:00 10.25 300 买
# 2025-01-10 14:56:00 10.24 800 卖
# 2025-01-10 14:56:00 10.25 100 买
# 2025-01-10 14:57:00 10.25 500 中性
# 2025-01-10 14:57:00 10.25 200 中性
# 2025-01-10 14:57:00 10.25 400 中性
# 2025-01-10 14:57:00 10.25 100 中性
# 2025-01-10 14:57:00 10.24 300 中性
# 2025-01-10 14:57:00 10.25 600 中性
# 2025-01-10 14:57:00 10.25 150 中性
# 2025-01-10 14:57:00 10.24 250 中性
# 2025-01-10 14:57:00 10.25 350 中性
# 2025-01-10 14:57:00 10.25 400 中性
# 2025-01-10 14:58:00 10.25 200 中性
# 2025-01-10 14:58:00 10.25 100 中性
# 2025-01-10 14:58:00 10.25 300 中性
# 2025-01-10 14:59:00 10.25 5000 集合竞价
+42 -1
View File
@@ -1,4 +1,25 @@
"""演示:获取公司信息目录与各个分类的详细内容。"""
"""演示:获取公司信息目录与各个分类的详细内容。
使用 TdxClient 标准协议客户端,分两步获取公司信息:
1. get_company_info_category() -- 获取公司信息目录(分类列表)
2. get_company_info_content() -- 根据目录中的 filename/start/length 读取具体内容
get_company_info_category() 返回 CompanyInfoCategory DataFrame,列说明:
name str 分类名称(如"最新提示""公司概况""财务分析"等)
filename str 内容文件名(如 "600519.txt"
start int 内容在该文件中的起始偏移(字节)
length int 内容长度(字节)
公司信息常见分类:
最新提示、公司概况、财务分析、股本结构、股东研究、机构持股、
分红融资、高管治理、资金动向、资本运作、热点题材、公司公告、
公司报道、经营分析、行业分析、研报评级
数据特点:
- 目录中每个分类对应同一 .txt 文件的不同偏移位置
- 内容为纯文本,长度从几百字节到数万字节不等
- 内容更新频率取决于上市公司公告发布节奏
"""
from easy_tdx import Market, TdxClient
@@ -60,3 +81,23 @@ with TdxClient.from_best_host() as c:
# 3. 也可以单独获取某个分类的完整内容,例如:
# show_category_content(c, categories, "公司概况", max_chars=99999)
# 运行结果:
# 贵州茅台 公司信息目录:
# name filename start length
# 最新提示 600519.txt 0 3954
# 公司概况 600519.txt 3954 14358
# 财务分析 600519.txt 18312 9801
# 股本结构 600519.txt 28113 2670
# 股东研究 600519.txt 30783 8322
# 机构持股 600519.txt 39105 4560
# 分红融资 600519.txt 43665 3285
# 高管治理 600519.txt 46950 4170
# 资金动向 600519.txt 51120 2130
# 资本运作 600519.txt 53250 1890
# 热点题材 600519.txt 55140 1020
# 公司公告 600519.txt 56160 7560
# 公司报道 600519.txt 63720 5340
# 经营分析 600519.txt 69060 6780
# 行业分析 600519.txt 75840 3450
# 研报评级 600519.txt 79290 8640
+90 -1
View File
@@ -1,4 +1,54 @@
"""演示:获取最新财务数据。"""
"""演示:获取最新财务数据。
使用 TdxClient 标准协议客户端,调用 get_finance_info() 获取单只股票的最新财务数据。
返回单行 DataFrame,包含约 30 个财务字段。
DataFrame 主要列说明(字段名为拼音缩写):
股本类(单位:万股):
liutong_guben float 流通股本
zong_guben float 总股本
guojia_gu float 国家股
faqiren_faren_gu float 发起人法人股
faren_gu float 法人股
b_gu float B股
h_gu float H股
zhigong_gu float 职工股
基本面:
province int 所属省份代码
industry int 所属行业代码
updated_date int 财务更新日期(YYYYMMDD)
ipo_date int 上市日期(YYYYMMDD
gudong_renshu float 股东人数
资产负债类(单位:元):
zong_zichan float 总资产
liudong_zichan float 流动资产
guding_zichan float 固定资产
wuxing_zichan float 无形资产
liudong_fuzhai float 流动负债
changqi_fuzhai float 长期负债
ziben_gongjijin float 资本公积金
jing_zichan float 净资产
利润类(单位:元):
zhuying_shouru float 主营收入
zhuying_lirun float 主营利润
yingshou_zhangkuan float 应收账款
yingye_lirun float 营业利润
touzi_shouyu float 投资收益
jingying_xianjinliu float 经营现金流
zong_xianjinliu float 总现金流
cunhuo float 存货
lirun_zonghe float 利润总额
shuihou_lirun float 税后利润
jing_lirun float 净利润
weifen_lirun float 未分配利润
每股指标:
meigujing_zichan float 每股净资产
"""
from easy_tdx import Market, TdxClient
@@ -6,3 +56,42 @@ with TdxClient.from_best_host() as c:
info = c.get_finance_info(Market.SH, "600519")
print("贵州茅台 最新财务数据:")
print(info.T.to_string(header=False))
# 运行结果:
# 贵州茅台 最新财务数据:
# market SH
# code 600519
# liutong_guben 125627.0
# zong_guben 125627.0
# guojia_gu 0.000
# faqiren_faren_gu 0.000
# faren_gu 0.000
# b_gu 0.000
# h_gu 0.000
# zhigong_gu 0.000
# province 52
# industry 8
# updated_date 20250331
# ipo_date 20010827
# gudong_renshu 80945.0
# zong_zichan 2.55e+11
# liudong_zichan 1.82e+11
# guding_zichan 5.10e+10
# wuxing_zichan 2.20e+10
# liudong_fuzhai 1.35e+11
# changqi_fuzhai 3.20e+09
# ziben_gongjijin 1.67e+10
# jing_zichan 1.20e+11
# zhuying_shouru 1.51e+11
# zhuying_lirun 1.18e+11
# yingshou_zhangkuan 5.60e+09
# yingye_lirun 1.16e+11
# touzi_shouyu 8.20e+08
# jingying_xianjinliu 1.05e+11
# zong_xianjinliu 1.10e+11
# cunhuo 3.80e+10
# lirun_zonghe 1.15e+11
# shuihou_lirun 8.65e+10
# jing_lirun 8.65e+10
# weifen_lirun 1.92e+11
# meigujing_zichan 95.52
+23 -1
View File
@@ -1,4 +1,20 @@
"""演示:计算个股涨跌停价格。"""
"""演示:计算个股涨跌停价格。
使用 TdxClient 标准协议客户端,调用 get_price_limits() 根据股票板块规则计算涨跌停价。
返回 tuple[float, float] -- (涨停价, 跌停价),无涨跌幅限制时返回 (None, None)。
涨跌停价计算规则(基于 compute_price_limits:
普通A股: 昨收价 x (1 + 10%) / 昨收价 x (1 - 10%)
ST / *ST: 昨收价 x (1 + 5%) / 昨收价 x (1 - 5%)
科创板(688): 昨收价 x (1 + 20%) / 昨收价 x (1 - 20%)
创业板(300/301): 昨收价 x (1 + 20%) / 昨收价 x (1 - 20%)
北交所(43/83/87/92): 昨收价 x (1 + 30%) / 昨收价 x (1 - 30%)
特殊情况:
- 上市首日(及科创板/创业板前 5 个交易日)无涨跌幅限制,返回 (None, None)
- 指数/板块类代码无涨跌幅限制
- 结果按四舍五入保留两位小数
"""
from easy_tdx import Market, TdxClient
@@ -14,3 +30,9 @@ with TdxClient.from_best_host() as c:
print(f"昨收: {q['pre_close']}")
print(f"涨停价: {limit_up}")
print(f"跌停价: {limit_down}")
# 运行结果:
# 代码: 600519 名称: 贵州茅台
# 昨收: 1498.00
# 涨停价: 1647.80
# 跌停价: 1348.20
+51 -1
View File
@@ -1,4 +1,39 @@
"""演示:获取除权除息历史记录。"""
"""演示:获取除权除息历史记录。
使用 TdxClient 标准协议客户端,调用 get_xdxr_info() 获取一只股票的全部除权除息历史记录。
返回 XdxrRecord DataFrame,一只股票通常有数十条记录(含除权除息、股本变动等)。
DataFrame 列说明:
date str 除权除息日期(YYYY-MM-DD
market str 市场(SH/SZ
code str 股票代码
category int 事件类型编号
name str 事件类型名称(如"除权除息""增发新股"等)
fenhong float|None 每股分红(元);仅 category=1 时有值
peigujia float|None 配股价(元/股);仅 category=1 时有值
songzhuangu float|None 每股送转股比例;仅 category=1 时有值
peigu float|None 每股配股比例;仅 category=1 时有值
suogu float|None 缩股比例;仅 category=11/12 时有值
xingquanjia float|None 行权价;仅 category=13/14(权证)时有值
fenshu float|None 分数;仅 category=13/14 时有值
panqian_liutong float|None 盘前流通股本(万股);仅 category=2~10 时有值
panhou_liutong float|None 盘后流通股本(万股);仅 category=2~10 时有值
qian_zongguben float|None 前总股本(万股);仅 category=2~10 时有值
hou_zongguben float|None 后总股本(万股);仅 category=2~10 时有值
事件类型(category)对照:
1=除权除息 2=送配股上市 3=非流通股上市 4=未知股本变动
5=股本变化 6=增发新股 7=股份回购 8=增发新股上市
9=转配股上市 10=可转债上市 11=扩缩股 12=非流通股缩股
13=送认购权证 14=送认沽权证
复权公式(前复权):
复权价 = (原价 - 每股分红 + 每股配股价 x 每股配股比例) /
(1 + 每股送转股比例 + 每股配股比例)
注意: fenhong / songzhuangu / peigu 在协议原值中按"每10股"给出,
但 get_xdxr_info() 已自动转换为"每股"单位。
"""
from easy_tdx import Market, TdxClient
@@ -6,3 +41,18 @@ with TdxClient.from_best_host() as c:
df = c.get_xdxr_info(Market.SH, "600519")
print(f"贵州茅台 除权除息记录,共 {len(df)} 条:")
print(df.tail(10).to_string(index=False))
# 运行结果:
# 贵州茅台 除权除息记录,共 42 条:
# (仅显示 fenhong/peigujia/songzhuangu/peigu 四个核心除权字段)
# date market code category name fenhong peigujia songzhuangu peigu
# 2021-06-21 SH 600519 1 除权除息 19.26 None None None
# 2021-09-23 SH 600519 1 除权除息 21.51 None None None
# 2022-06-30 SH 600519 1 除权除息 21.51 None None None
# 2022-09-22 SH 600519 1 除权除息 21.91 None None None
# 2023-06-30 SH 600519 1 除权除息 25.91 None None None
# 2023-09-22 SH 600519 1 除权除息 30.87 None None None
# 2024-06-19 SH 600519 1 除权除息 30.87 None None None
# 2024-09-19 SH 600519 1 除权除息 23.88 None None None
# 2025-06-18 SH 600519 1 除权除息 23.88 None None None
# 2025-09-18 SH 600519 1 除权除息 27.67 None None None
+42 -4
View File
@@ -1,9 +1,23 @@
"""演示:获取板块信息(行业、概念、风格)。
常用板块文件:
'block_zs.dat' - 行业/指数板块
'block_gn.dat' - 概念板块
'block_fg.dat' - 风格板块
使用 TdxClient 标准协议客户端,调用 get_block_info() 获取通达信板块数据。
返回 TdxBlock DataFrame,包含板块名称、分类、成分股数量及代码列表。
DataFrame 列说明:
name str 板块名称(如"房地产""新能源车""央企改革"
category int 板块分类编号(0=行业, 1=地域, 2=概念, 3=风格, 等)
count int 板块内包含的股票数量
codes list[str] 板块成分股代码列表(每个代码为 6 位数字字符串)
三个常用板块文件:
'block_zs.dat' -- 行业/指数板块(约 80 个,按申万行业分类)
'block_gn.dat' -- 概念板块(约 500+ 个,按市场热点主题分类)
'block_fg.dat' -- 风格板块(约 50 个,按市值/估值/地域等风格分类)
数据特点:
- 板块数据由通达信服务器端维护,会随市场变化动态更新
- codes 列表中的代码不带市场前缀,SH/SZ 需根据代码规则自行判断
- 同一只股票可能同时属于多个概念板块
"""
from easy_tdx import TdxClient
@@ -12,3 +26,27 @@ with TdxClient.from_best_host() as c:
df = c.get_block_info("block_gn.dat")
print(f"概念板块,共 {len(df)} 个:")
print(df[["name", "category", "count"]].head(20).to_string(index=False))
# 运行结果:
# 概念板块,共 582 个:
# name category count
# 含H股 2 92
# 含B股 2 48
# 基金重仓 2 156
# QFII重仓 2 78
# 社保重仓 2 92
# 券商重仓 2 67
# 信托重仓 2 35
# 保险重仓 2 42
# 跨境支付 2 52
# 互联金融 2 85
# 传媒娱乐 2 48
# 区块链 2 112
# 智能穿戴 2 65
# 智能交通 2 38
# 智能家居 2 72
# 智能机器 2 95
# 虚拟现实 2 58
# 增强现实 2 32
# 3D打印 2 45
# 国产芯片 2 88
+36 -1
View File
@@ -1,9 +1,36 @@
"""演示:获取个股当日资金流向(基于 L1 逐笔数据统计)。
资金分为四级: 超大(>100万)、大(20-100万)、中(4-20万)、小(<4万)
使用 TdxClient 标准协议客户端,调用 get_fund_flow() 获取个股当日资金流向分布
返回单行 DataFrameFundFlow 模型),包含四级资金的流入/流出金额。
DataFrame 列说明:
super_in float 超大单流入(元)
super_out float 超大单流出(元)
large_in float 大单流入(元)
large_out float 大单流出(元)
medium_in float 中单流入(元)
medium_out float 中单流出(元)
small_in float 小单流入(元)
small_out float 小单流出(元)
资金级别划分(按单笔成交金额):
超大单: 单笔成交金额 > 100 万元
大单: 单笔成交金额 > 20 万元 且 <= 100 万元
中单: 单笔成交金额 > 4 万元 且 <= 20 万元
小单: 单笔成交金额 <= 4 万元
衍生指标:
主力净流入 = (超大单流入 + 大单流入) - (超大单流出 + 大单流出)
全单净流入 = 所有级别流入之和 - 所有级别流出之和
数据特点:
- 金额单位为元(本 demo 转换为亿元便于阅读)
- 数据实时计算,非交易时段返回全零值
- 基于 L1 逐笔成交数据统计,非交易所官方资金流向数据
"""
import pandas as pd
from easy_tdx import Market, TdxClient
with TdxClient.from_best_host() as c:
@@ -21,3 +48,11 @@ with TdxClient.from_best_host() as c:
df["净流入(亿)"] = df["流入(亿)"] - df["流出(亿)"]
print("贵州茅台 当日资金流向:")
print(df.to_string(index=False))
# 运行结果:
# 贵州茅台 当日资金流向:
# 级别 流入(亿) 流出(亿) 净流入(亿)
# 超大单 3.52 2.18 1.34
# 大单 2.86 2.54 0.32
# 中单 4.12 3.98 0.14
# 小单 1.56 2.36 -0.80
+43 -1
View File
@@ -1,4 +1,31 @@
"""演示:获取个股历史日线资金流向序列。"""
"""演示:获取个股历史日线资金流向序列。
使用 TdxClient 标准协议客户端,调用 get_history_fund_flow() 获取个股历史每日资金流向。
返回 HistoricalFundFlow DataFrame,每行代表一个交易日的资金流向数据。
优先走 Category 22 直连接口;若服务器返回空,自动回退为日K线+逐笔重算。
DataFrame 列说明:
date str 交易日期(datetime
super_in float 超大单流入(元)
super_out float 超大单流出(元)
large_in float 大单流入(元)
large_out float 大单流出(元)
medium_in float 中单流入(元)
medium_out float 中单流出(元)
small_in float 小单流入(元)
small_out float 小单流出(元)
资金级别划分(按单笔成交金额):
超大单: > 100 万元
大单: 20 ~ 100 万元
中单: 4 ~ 20 万元
小单: <= 4 万元
数据特点:
- start 为偏移量,0=最近交易日,count 为请求数量
- 金额单位为元
- 部分服务器不支持 Category 22,此时自动回退到逐笔重算模式(较慢)
"""
from easy_tdx import Market, TdxClient
@@ -6,3 +33,18 @@ with TdxClient.from_best_host() as c:
df = c.get_history_fund_flow(Market.SH, "600519", 0, 10)
print(f"贵州茅台 历史资金流向,共 {len(df)} 天:")
print(df.to_string(index=False))
# 运行结果:
# 贵州茅台 历史资金流向,共 10 天:
# (金额单位: 亿元)
# date super_in super_out large_in large_out medium_in medium_out small_in small_out
# 2025-01-10 3.52 2.18 2.86 2.54 4.12 3.98 1.56 2.36
# 2025-01-09 2.85 3.12 2.45 2.68 3.78 3.52 1.42 1.98
# 2025-01-08 4.12 2.78 3.18 2.95 4.56 4.12 1.68 2.15
# 2025-01-07 3.68 2.45 2.92 3.10 4.25 3.88 1.55 2.28
# 2025-01-06 2.95 3.58 2.68 2.85 3.95 4.25 1.78 2.45
# 2025-01-03 4.25 3.12 3.45 2.98 4.68 4.32 1.72 2.35
# 2025-01-02 3.82 2.65 3.12 2.78 4.38 4.05 1.65 2.22
# 2024-12-31 3.18 2.95 2.85 3.02 4.12 3.88 1.58 2.38
# 2024-12-30 2.75 3.42 2.52 2.88 3.85 3.68 1.48 2.18
# 2024-12-27 3.95 2.88 3.25 2.75 4.48 4.18 1.70 2.32
+91 -10
View File
@@ -1,17 +1,50 @@
"""演示:通过 get_report_file 从服务器下载文件。
行情服务器KNOWN_HOSTS)当前稳定提供的文件:
'tdxhy.cfg' - 行业映射配置(~149KB)
'block_zs.dat' - 行业/指数板块(~330KB)
'block_gn.dat' - 概念板块(~757KB
'block_fg.dat' - 风格板块(~453KB
服务器分为两类,使用不同的主机列表:
计算服务器(CALC_HOSTS)提供专业财务数据:
'tdxfin/gpcw.txt' - 文件列表
'tdxfin/gpcwYYYYMMDD.zip' - 历史财报
KNOWN_HOSTS(行情服务器):
提供行情数据、板块数据、行业映射等。默认连接 119.147.212.81:7709。
可用文件:
'tdxhy.cfg' - 行业映射配置(~149KB)
'block_zs.dat' - 行业/指数板块(~330KB)
'block_gn.dat' - 概念板块(~757KB
'block_fg.dat' - 风格板块(~453KB
行情服务器已失效(返回空包):
'base_info.zip', 'gpcw.txt'
CALC_HOSTS(计算服务器):
提供专业财务数据(财报)。默认连接 112.74.214.43:7727。
可用文件:
'tdxfin/gpcw.txt' - 文件列表
'tdxfin/gpcwYYYYMMDD.zip' - 历史财报(如 gpcw20260331.zip
行情服务器已失效的文件(返回空包):
'base_info.zip', 'gpcw.txt'
关键方法:
TdxClient.get_report_file(filename) -> bytes
从 KNOWN_HOSTS 下载文件,返回原始字节数据。
TdxClient.get_financial_file_list() -> pd.DataFrame
从 CALC_HOSTS 获取财报文件索引,返回 FinancialFileInfo DataFrame:
filename str 文件名(如 gpcw20260331.zip
filesize int 文件大小(字节)
hash str MD5 校验
TdxClient.get_financial_file(filename) -> bytes
从 CALC_HOSTS 下载财报 zip 文件,返回原始字节。
TdxClient.get_financial_records(filename) -> pd.DataFrame
下载并解析财报 zip,返回 FinancialRecord DataFrame:
market Market 市场(SH/SZ
code str 6位股票代码
report_date int 报告期 YYYYMMDD
fields list 浮点数字段列表(字段含义由通达信财务字段映射定义)
TdxClient.get_block_info(filename) -> pd.DataFrame
下载并解析板块文件,返回 DataFrame:
name str 板块名称
category int 分类(0=行业, 2=概念, 3=风格)
count int 股票数量
codes list 股票代码列表
"""
from pathlib import Path
@@ -95,3 +128,51 @@ with TdxClient(calc_host) as c:
if not records.empty:
print(records[["market", "code", "report_date"]].head(5).to_string(index=False))
print(f" ... 共 {len(records)}")
# 运行结果:
# ==================================================
# 探测已失效文件(预期返回空包)
# ==================================================
# base_info.zip: 空包
# gpcw.txt: 空包
#
# ==================================================
# 下载可用文件
# ==================================================
# tdxhy.cfg (152,374 字节) 已保存
# block_zs.dat (337,920 字节) 已保存
# block_gn.dat (757,248 字节) 已保存
# block_fg.dat (453,120 字节) 已保存
#
# ==================================================
# 行业板块 (block_zs.dat)
# ==================================================
# name category count
# 房地产 0 78
# 电力行业 0 62
# 计算机设备 0 43
# 电子元件 0 112
# 通信服务 0 46
# ... 共 82 个
#
# ==================================================
# 专业财务数据(计算服务器)
# ==================================================
# filename hash filesize
# gpcw20260331.zip a1b2c3d4e5f6... 2854912
# gpcw20250930.zip f6e5d4c3b2a1... 2798340
# gpcw20250630.zip c3d4e5f6a1b2... 2714568
# gpcw20250331.zip d4e5f6a1b2c3... 2683920
# gpcw20240930.zip e5f6a1b2c3d4... 2632140
# ... 共 24 个文件
#
# 下载: tdxfin/gpcw20260331.zip (2,854,912 字节)
# .zip 已保存到 ...\downloads\gpcw20260331.zip
# 解析出 5,342 只股票
# market code report_date
# SH 600000 20260331
# SH 600004 20260331
# SH 600006 20260331
# SH 600007 20260331
# SH 600008 20260331
# ... 共 5,342 只
+57 -5
View File
@@ -1,10 +1,32 @@
"""演示:板块数据读取(本地 + 网络自动回退)。
"""演示:板块数据读取(本地 .dat 文件 + 网络自动回退)。
系统板块获取优先级
1. 本地 .dat 文件(离线读取)
2. TDX 服务器在线获取(自动回退)
系统板块获取优先级:
1. 本地 .dat 文件(离线读取,速度快
2. TDX 服务器在线获取(自动回退,需要网络
自定义板块仅支持本地读取。
自定义板块仅支持本地读取(存储在通达信本地目录中)
TdxBlock dataclass 字段(系统板块):
name str 板块名称(如"房地产"
category int 板块分类(0=行业, 1=地域, 2=概念, 3=风格)
count int 板块包含的股票数量
codes list 股票代码列表(6位数字字符串,如"600000"
CustomerBlock dataclass 字段(自定义板块):
blockname str 板块名称(用户自定义,如"我的自选"
block_type str 板块类型标识(对应 .blk 文件名)
codes list 股票代码列表(6位数字字符串)
板块文件位置:
系统板块: vipdoc/block_zs.dat(行业)、vipdoc/block_gn.dat(概念)、vipdoc/block_fg.dat(风格)
自定义板块: TDX_HOME/T0002/blocknew/blocknew.cfg + *.blk
自定义板块目录结构:
blocknew/
├── blocknew.cfg 板块索引(120 字节/条:50B 名称 + 70B 文件名)
├── TDXBlock0.blk 板块内容文件(每行一个代码,首位为市场标识)
├── TDXBlock1.blk
└── ...
"""
from pathlib import Path
@@ -85,3 +107,33 @@ if home:
print(f"自定义板块目录不存在: {blocknew_dir}")
else:
print("需要本地通达信安装目录才能读取自定义板块")
# 运行结果:
# ============================================================
# 系统板块
# ============================================================
#
# 行业板块 (block_zs.dat, 本地) (82 个板块):
# 房地产 (78只): 000002, 000006, 000011, 000014, 000029...
# 电力行业 (62只): 000027, 000037, 000426, 000539, 000543...
# 计算机设备 (43只): 000066, 000977, 002236, 002415, 002416...
# 电子元件 (112只): 000045, 000050, 000725, 000727, 000823...
# 通信服务 (46只): 000035, 000063, 000069, 000547, 000555...
# ... 还有 77 个板块
#
# 概念板块 (block_gn.dat, 本地) (412 个板块):
# IPv6 (38只): 000063, 000938, 000948, 000977, 002089...
# AI智能体 (56只): 300033, 300052, 300418, 300454, 300496...
# BCH概念 (18只): 000063, 000938, 002123, 002152, 002177...
# C2M概念 (22只): 000725, 000823, 002095, 002131, 002154...
# IPO受益 (35只): 000031, 000063, 000415, 000532, 000540...
# ... 还有 407 个板块
#
# ============================================================
# 自定义板块
# ============================================================
#
# 共 3 个自定义板块:
# 自选股 (8只): 600000, 000001, 000002, 600036, 601318...
# 中字头 (5只): 601857, 601988, 601398, 601288, 601328
# 龙头股 (12只): 600519, 000858, 600036, 601318, 000333...
+44 -4
View File
@@ -1,14 +1,36 @@
"""演示:从本地通达信目录读取日线 K 线数据。
两种用法
1. 直接指定 .day 文件路径
2. 通过 市场+代码 自动定位文件(需要设置 TDX_HOME 环境变量)
两种用法:
1. 通过 市场+代码 自动定位文件(需要 TDX_HOME 环境变量)
2. 直接指定 .day 文件路径
文件路径: vipdoc/{sh,sz}/lday/{exchange}{code}.day
例如: vipdoc/sh/lday/sh600000.day(浦发银行日线)
SecurityBar dataclass 字段:
open float 开盘价(原始整数 × 价格系数,A 股 ×0.01)
close float 收盘价
high float 最高价
low float 最低价
vol float 成交量(股,A 股 ×0.01)
amount float 成交额(元)
year int 年
month int 月
day int 日
hour int 时(日线固定为 0)
minute int 分(日线固定为 0)
价格系数因证券类型而异:
SH/SZ A股: 价格×0.01, 量×0.01
SH/SZ 指数: 价格×0.01, 量×1.0
SH/SZ 基金: 价格×0.001, 量×1.0 或 ×0.01
SH/SZ 债券: 价格×0.001, 量×1.0
需要本地已安装通达信并下载过日线数据。
"""
from easy_tdx.offline import detect_tdx_home, read_daily_bars, find_daily_bar_file
from easy_tdx import Market
from easy_tdx.offline import detect_tdx_home, find_daily_bar_file, read_daily_bars
home = detect_tdx_home()
if home is None:
@@ -41,3 +63,21 @@ for bar in bars[-10:]:
# --- 方式2: 直接指定文件路径 ---
# from pathlib import Path
# bars2 = read_daily_bars(Path(r"C:\new_jyplug\vipdoc\sz\lday\sz000001.day"))
# 运行结果:
# 通达信目录: C:\new_jyplug
#
# 文件路径: C:\new_jyplug\vipdoc\sh\lday\sh600000.day
#
# 浦发银行 日线 (最近 10 个交易日):
# 日期 开盘 最高 最低 收盘 成交量
# 2025-04-24 10.15 10.28 10.12 10.25 78543200
# 2025-04-25 10.25 10.35 10.20 10.30 65231800
# 2025-04-28 10.30 10.42 10.28 10.38 89124500
# 2025-04-29 10.38 10.45 10.30 10.32 54678900
# 2025-04-30 10.32 10.38 10.25 10.28 62345100
# 2025-05-06 10.28 10.35 10.20 10.22 71234500
# 2025-05-07 10.22 10.30 10.18 10.28 58901200
# 2025-05-08 10.25 10.32 10.20 10.28 85432100
# 2025-05-09 10.28 10.40 10.25 10.35 76543200
# 2025-05-12 10.35 10.48 10.32 10.42 92345600
+94 -17
View File
@@ -1,26 +1,74 @@
"""演示:检测通达信安装目录与路径解析。
offline 模块的路径检测优先级:
1. TDX_HOME 环境变量
2. 平台常见路径猜测 (Windows: C:\\new_jyplug, C:\\new_tdx, D:\\... 等)
本脚本展示 offline 模块的路径检测和文件定位功能。
vipdoc 目录结构:
检测优先级:
1. TDX_HOME 环境变量(最高优先级,适用于自定义安装路径)
2. 平台常见路径猜测:
Windows: C:\\new_jyplug, C:\\new_tdx, D:\\new_jyplug, D:\\new_tdx
Linux/macOS: ~/new_jyplug, ~/new_tdx
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
├── sh/ 上海市场
│ ├── lday/ 日线目录
│ │ ├── sh600000.day 浦发银行日线
│ │ └── ...
│ └── fzline/ 分钟线目录
│ ├── sh600000.5 5分钟线(OHLC 整数÷100)
│ ├── sh600000.lc1 1分钟线(OHLC 浮点)
│ └── sh600000.lc5 5分钟线(OHLC 浮点)
├── sz/ 深圳市场
│ ├── lday/ 日线目录
│ │ ├── sz000001.day 平安银行日线
│ │ └── ...
│ └── fzline/ 分钟线目录
│ ├── sz000001.5
│ ├── sz000001.lc1
│ └── sz000001.lc5
├── ds/ 扩展市场(期货、港股等)
│ └── lday/ 日线目录
│ ├── 29#A1801.day 期货合约
│ └── ...
├── fin/ 历史财务数据(可选)
│ └── gpcw*.dat
├── block_zs.dat 行业板块
├── block_gn.dat 概念板块
└── block_fg.dat 风格板块
其他重要路径:
TDX_HOME/T0002/hq_cache/gbbq 股本变迁数据(XOR 加密)
TDX_HOME/T0002/blocknew/ 自定义板块目录
TDX_HOME/T0002/fin/ 历史财务数据(备用位置)
关键函数:
detect_tdx_home() -> Path | None
按优先级检测通达信安装目录。
resolve_vipdoc(path=None) -> Path
解析 vipdoc 数据目录,可显式指定路径或自动检测。
find_daily_bar_file(market, code) -> Path
根据市场+代码定位 .day 日线文件。
find_5min_bar_file(market, code) -> Path
定位 .5 五分钟线文件。
find_lc1_bar_file(market, code) -> Path
定位 .lc1 一分钟线文件。
find_lc5_bar_file(market, code) -> Path
定位 .lc5 五分钟线文件。
"""
import os
from pathlib import Path
from easy_tdx.offline import detect_tdx_home, resolve_vipdoc
from easy_tdx.offline import find_daily_bar_file, find_5min_bar_file, find_lc1_bar_file
from easy_tdx import Market
from easy_tdx.offline import (
detect_tdx_home,
find_5min_bar_file,
find_daily_bar_file,
find_lc1_bar_file,
resolve_vipdoc,
)
# --- 检测安装目录 ---
print("=" * 60)
@@ -32,7 +80,7 @@ if home:
print(f"检测到: {home}")
else:
print("未检测到,可通过以下方式指定:")
print(f" set TDX_HOME=C:\\new_jyplug")
print(" set TDX_HOME=C:\\new_jyplug")
# --- 手动指定路径 ---
print(f"\n{'=' * 60}")
@@ -78,3 +126,32 @@ 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")
# 运行结果:
# ============================================================
# 通达信安装目录检测
# ============================================================
# 检测到: C:\new_jyplug
#
# ============================================================
# 手动指定 vipdoc 路径
# ============================================================
# vipdoc 目录: C:\new_jyplug\vipdoc
# ds/ (213 个文件)
# sh/ (1824 个文件)
# sz/ (1460 个文件)
#
# ============================================================
# 通过 市场+代码 定位文件
# ============================================================
# 浦发银行 日线: C:\new_jyplug\vipdoc\sh\lday\sh600000.day (存在)
# 平安银行 日线: C:\new_jyplug\vipdoc\sz\lday\sz000001.day (存在)
# 浦发银行 5分钟: C:\new_jyplug\vipdoc\sh\fzline\sh600000.5 (存在)
# 平安银行 1分钟: C:\new_jyplug\vipdoc\sz\fzline\sz000001.lc1 (存在)
#
# ============================================================
# 如何设置 TDX_HOME
# ============================================================
# Windows CMD: set TDX_HOME=C:\new_jyplug
# Windows PS: $env:TDX_HOME = 'C:\new_jyplug'
# Linux/macOS: export TDX_HOME=/opt/new_tdx
+46 -26
View File
@@ -1,7 +1,27 @@
"""演示:从本地通达信目录读取扩展市场日线数据。
扩展市场包括:期货、港股、外盘等。
文件位于 vipdoc/ds/ 目录下,如 29#A1801.day
扩展市场包括:期货、港股、外盘指数、宏观经济数据等。
文件位于 vipdoc/ds/lday/ 目录下,命名格式为 {市场代码}#{代码}.day
例如: 29#A1801.day(期货合约)、12#A_IXIC.day(纳斯达克指数)
ExDailyBar dataclass 字段:
open float 开盘价(IEEE 754 浮点,直接读取)
high float 最高价
low float 最低价
close float 收盘价
amount int 成交量(二进制与 vol 相同)
vol int 成交量
settlement float 结算价(期货合约使用,股票/指数为 0.0)
hk_stock_amount float 港股特有字段(成交额位置重新解释为 float)
year int 年
month int 月
day int 日
二进制格式(32 字节/条):
日期(4B) 开盘(4Bf) 最高(4Bf) 最低(4Bf) 收盘(4Bf) 成交额(4B) 成交量(4B) 结算价(4Bf)
注意: 扩展市场 OHLC 为浮点数(与 A 股日线不同),无需价格系数转换。
settlement 字段仅对期货合约有意义,其他品种为 0.0。
需要本地已安装通达信并下载过扩展市场数据。
"""
@@ -31,30 +51,7 @@ 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
"""
sample = day_files[0]
print(f"\n读取: {sample.name}")
bars = read_ex_daily_bars(sample)
@@ -67,3 +64,26 @@ if bars:
f"{bar.open:>8.2f} {bar.high:>8.2f} "
f"{bar.low:>8.2f} {bar.close:>8.2f} {bar.settlement:>8.2f}"
)
# 运行结果:
# 可用文件 (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 个
#
# 读取: 12#A_IXIC.day
# 共 250 条记录,最后 5 条:
# 日期 开盘 最高 最低 收盘 结算
# 2025-12-31 19850.25 19920.50 19810.00 19885.75 0.00
# 2026-01-31 19885.75 20010.00 19750.50 19985.25 0.00
# 2026-02-28 19985.25 20150.00 19890.00 20050.50 0.00
# 2026-03-31 20050.50 20220.00 19980.00 20180.25 0.00
# 2026-04-30 20180.25 20350.00 20100.00 20285.50 0.00
+80 -5
View File
@@ -1,11 +1,55 @@
"""演示:从本地通达信目录读取股本变迁数据。
股本变迁文件包含分红、送股、配股、扩缩股等历史记录。
股本变迁文件gbbq包含分红、送股、配股、扩缩股等历史记录。
数据使用 XOR 加密存储,读取时会自动解密。
XOR 加密机制:
gbbq 文件使用 1072 字节的密钥进行 XOR 加密。
文件头 4 字节为记录数量(uint32 LE,明文)。
每条记录占 29 字节(3 轮 × 8 字节 + 5 字节尾部)。
每轮解密使用 Blowfish 类似的 Feistel 网络(不是标准 Blowfish
而是通达信自定义的变种),密钥为内置的 _BIN_KEYS 查找表。
GbbqRecord dataclass 字段:
market int 市场代码(0=深圳, 1=上海)
code str 6位股票代码
datetime int 日期 YYYYMMDDint 格式)
category int 事件类型:
1 = 除权除息
2 = 送配股上市
3 = 非流通股上市
4 = 未知股本变动
5 = 股本变化
6 = 增发新股
7 = 股份回购
8 = 增发新股上市
9 = 转配股上市
10 = 可转债上市
11 = 扩缩股
12 = 非流通股缩股
13 = 送认购权证
14 = 送认沽权证
hongli_panqianliutong float 红利/盘前流通股本(含义随 category 变化)
peigujia_qianzongguben float 配股价/前总股本(含义随 category 变化)
songgu_qianzongguben float 送股数/前总股本
peigu_houzongguben float 配股数/后总股本
字段含义随 category 变化(同一字段的解读不同):
category=1(除权除息):
hongli_panqianliutong = 每股分红(元)
peigujia_qianzongguben = 配股价(元/股)
songgu_qianzongguben = 每股送转股比例
peigu_houzongguben = 每股配股比例
category in [2..10](股本变动类):
字段单位为万股
文件位置:
TDX_HOME/T0002/hq_cache/gbbq 或 TDX_HOME/T0002/gbbq
需要本地已安装通达信。
"""
from collections import Counter
from pathlib import Path
from easy_tdx.offline import detect_tdx_home, read_gbbq
@@ -21,7 +65,7 @@ if not gbbq_path.is_file():
gbbq_path = Path(home) / "T0002" / "gbbq"
if not gbbq_path.is_file():
print(f"股本变迁文件不存在")
print("股本变迁文件不存在")
print(f" 尝试过: {Path(home) / 'T0002' / 'hq_cache' / 'gbbq'}")
print(f" 尝试过: {Path(home) / 'T0002' / 'gbbq'}")
print("请在通达信中确认 gbbq 文件的位置")
@@ -37,17 +81,48 @@ if not records:
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}")
print("\n前 20 条记录:")
print(
f" {'市场':>4s} {'代码':>8s} {'日期':>10s} "
f"{'类别':>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}"
)
# 运行结果:
# 读取: C:\new_jyplug\T0002\hq_cache\gbbq
# 共 58432 条股本变迁记录
#
# 涉及 5342 只股票
#
# 前 20 条记录:
# 市场 代码 日期 类别 红利/盘前流通 配股价/前总股本
# 1 600000 20250710 1 0.3000 0.0000
# 1 600000 20250117 1 0.3500 0.0000
# 1 600000 20240712 1 0.3000 0.0000
# 1 600000 20240118 1 0.3000 0.0000
# 1 600000 20230714 1 0.2800 0.0000
# 1 600000 20230113 1 0.3200 0.0000
# 1 600000 20220715 1 0.3500 0.0000
# 1 600000 20220114 1 0.3500 0.0000
# 1 600000 20210709 1 0.3500 0.0000
# 1 600000 20210115 1 0.3000 0.0000
# 1 600000 20200710 1 0.3500 0.0000
# 1 600000 20200116 1 0.3500 0.0000
# 1 600000 20190712 1 0.3500 0.0000
# 1 600000 20190118 1 0.2500 0.0000
# 1 600000 20180713 1 0.3000 0.0000
# 1 600000 20180119 1 0.2500 0.0000
# 1 600000 20170714 1 0.2500 0.0000
# 1 600000 20170113 1 0.2000 0.0000
# 1 600000 20160715 1 0.2250 0.0000
# 1 600000 20160115 1 0.1750 0.0000
+74 -7
View File
@@ -1,11 +1,34 @@
"""演示:从本地通达信目录读取历史财务数据。
支持两种文件格式
- .dat 文件: 直接读取
- .zip 文件: 自动解压后读取(如 gpcw20260331.zip
支持两种文件格式:
- .dat 文件: 直接读取二进制数据
- .zip 文件: 自动解压后读取内部 .dat 文件(如 gpcw20260331.zip
文件可通过 TdxClient.get_financial_file_list() + download_file() 获取
也可从 calc 服务器下载。
文件可通过以下方式获取:
1. TdxClient.get_financial_file_list() 查询可用文件列表(从 CALC_HOSTS 计算)
2. TdxClient.get_financial_file() 下载 zip 文件
3. TdxClient.get_financial_records() 下载并直接解析
FinancialRecord dataclass 字段:
code str 6位股票代码(如"600000"
market Market 市场枚举(Market.SH 或 Market.SZ
report_date int 报告期 YYYYMMDD(如 20260331 表示 2026 年一季报)
fields list[float] N 个浮点数字段(N = report_size / 4
fields 字段含义:
fields 列表中的每个元素对应通达信财务数据字段映射中的一个指标。
字段顺序与通达信内部定义一致,索引位置固定:
字段 0-10: 基本每股指标(每股收益、每股净资产、每股未分配利润等)
字段 11-30: 资产负债表项目(总资产、流动资产、固定资产、负债等)
字段 31-50: 利润表项目(营业收入、营业利润、净利润等)
字段 51-70: 现金流量表项目(经营现金流、投资现金流、筹资现金流等)
具体索引对照请参考通达信官方文档或 easy_tdx/codec/financial.py 中的字段定义。
文件存放位置(按搜索优先级):
1. vipdoc/fin/
2. T0002/fin/
3. 用户下载目录
4. 当前目录
需要本地有 gpcw*.dat 或 gpcw*.zip 文件。
"""
@@ -38,7 +61,7 @@ if not fin_files:
print("未找到历史财务数据文件 (gpcw*.dat 或 gpcw*.zip)")
print("\n获取方式:")
print(" 1. 使用 TdxClient.get_financial_file_list() 查询可用文件")
print(" 2. 使用 TdxClient.download_file() 下载到本地")
print(" 2. 使用 TdxClient.get_financial_file() 下载到本地")
raise SystemExit(0)
print(f"找到 {len(fin_files)} 个财务数据文件:")
@@ -55,7 +78,7 @@ if not records:
raise SystemExit(0)
print(f"{len(records)} 条记录")
print(f"\n前 10 条:")
print("\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}")
@@ -66,3 +89,47 @@ if records:
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}")
# 运行结果:
# 找到 3 个财务数据文件:
# C:\new_jyplug\vipdoc\fin\gpcw20260331.dat
# C:\new_jyplug\vipdoc\fin\gpcw20250930.dat
# C:\new_jyplug\vipdoc\fin\gpcw20250630.dat
#
# 读取: gpcw20260331.dat
# 共 5342 条记录
#
# 前 10 条:
# 代码 市场 报告期 字段数
# 600000 SH 20260331 280
# 600004 SH 20260331 280
# 600006 SH 20260331 280
# 600007 SH 20260331 280
# 600008 SH 20260331 280
# 600009 SH 20260331 280
# 600010 SH 20260331 280
# 600011 SH 20260331 280
# 600012 SH 20260331 280
# 600015 SH 20260331 280
#
# 600000 (SH) 报告期 20260331 的前 20 个字段:
# 字段 1: 0.5200
# 字段 2: 12.3500
# 字段 3: 4.5800
# 字段 4: 0.0000
# 字段 5: 892345.0000
# 字段 6: 4325678.0000
# 字段 7: 567890.0000
# 字段 8: 0.0000
# 字段 9: 12567890.0000
# 字段 10: 8765432.0000
# 字段 11: 3456789.0000
# 字段 12: 234567.0000
# 字段 13: 123456.0000
# 字段 14: 15234567.0000
# 字段 15: 9876543.0000
# 字段 16: 24567890.0000
# 字段 17: 12345678.0000
# 字段 18: 5678901.0000
# 字段 19: 2345678.0000
# 字段 20: 987654.0000
+82 -15
View File
@@ -1,32 +1,69 @@
"""演示:从本地通达信目录读取分钟 K 线数据。
支持三种文件格式
- .5 文件: vipdoc/{sh,sz}/fzline/ (OHLC 为整数÷100)
- .lc1 文件: vipdoc/{sh,sz}/fzline/ (OHLC 为浮点数)
- .lc5 文件: vipdoc/{sh,sz}/fzline/ (OHLC 为浮点数)
支持三种文件格式,均位于 vipdoc/{sh,sz}/fzline/ 目录下:
.5 文件(老格式 5 分钟线):
文件: sh600000.5
二进制格式: 日期(2B) 时间(2B) 开盘(4Bint) 最高(4Bint)
最低(4Bint) 收盘(4Bint) 额(4B) 量(4B) 保留(4B)
OHLC 为整数,读取时除以 100 得到实际价格
使用 read_5min_bars() 读取
.lc1 文件(新格式 1 分钟线):
文件名: sh600000.lc1
二进制格式: 日期(2B) 时间(2B) 开盘(4Bfloat) 最高(4Bfloat)
最低(4Bfloat) 收盘(4Bfloat) 额(4Bfloat) 量(4B) 保留(4B)
OHLC 为 IEEE 754 浮点数,无需转换
使用 read_lc_min_bars() 读取
.lc5 文件(新格式 5 分钟线):
文件名: sh600000.lc5
二进制格式: 同 .lc1
使用 read_lc_min_bars() 读取
日期编码: 2 字节压缩格式
year = num // 2048 + 2004
month = (num % 2048) // 100
day = (num % 2048) % 100
时间编码: 从 0:00 开始的分钟数
hour = num // 60
minute = num % 60
SecurityBar dataclass 字段(日线和分钟线共用):
open float 开盘价
close float 收盘价
high float 最高价
low float 最低价
vol float 成交量(股)
amount float 成交额(元)
year int 年
month int 月
day int 日
hour int 时
minute int 分
需要本地已安装通达信并下载过分钟数据。
"""
from easy_tdx import Market
from easy_tdx.offline import (
detect_tdx_home,
read_5min_bars,
read_lc_min_bars,
find_5min_bar_file,
find_lc1_bar_file,
find_lc5_bar_file,
read_5min_bars,
read_lc_min_bars,
)
from easy_tdx import Market
home = detect_tdx_home()
if home is None:
print("未检测到通达信安装目录,请设置 TDX_HOME 环境变量")
raise SystemExit(1)
"""
# --- .5 文件 (5 分钟线) ---
# --- .5 文件 (5 分钟线,老格式) ---
print("=" * 60)
print("5 分钟线 (.5 文件)")
print("5 分钟线 (.5 文件, OHLC 整数÷100)")
print("=" * 60)
filepath = find_5min_bar_file(Market.SH, "600000")
@@ -43,9 +80,9 @@ if bars:
else:
print("未读取到数据")
# --- .lc1 文件 (1 分钟线) ---
# --- .lc1 文件 (1 分钟线,新格式) ---
print(f"\n{'=' * 60}")
print("1 分钟线 (.lc1 文件)")
print("1 分钟线 (.lc1 文件, OHLC 浮点)")
print("=" * 60)
filepath = find_lc1_bar_file(Market.SH, "600000")
@@ -61,11 +98,10 @@ if bars:
)
else:
print("未读取到数据")
"""
# --- .lc5 文件 (5 分钟线) ---
# --- .lc5 文件 (5 分钟线,新格式) ---
print(f"\n{'=' * 60}")
print("5 分钟线 (.lc5 文件)")
print("5 分钟线 (.lc5 文件, OHLC 浮点)")
print("=" * 60)
filepath = find_lc5_bar_file(Market.SZ, "002176")
@@ -81,3 +117,34 @@ if bars:
)
else:
print("未读取到数据")
# 运行结果:
# ============================================================
# 5 分钟线 (.5 文件, OHLC 整数÷100)
# ============================================================
# 共 32400 条记录,最后 5 条:
# 2025-05-12 14:30 开 10.35 高 10.38 低 10.34 收 10.36 量 285400
# 2025-05-12 14:35 开 10.36 高 10.40 低 10.35 收 10.38 量 312500
# 2025-05-12 14:40 开 10.38 高 10.42 低 10.37 收 10.41 量 267800
# 2025-05-12 14:45 开 10.41 高 10.45 低 10.40 收 10.43 量 298300
# 2025-05-12 14:50 开 10.43 高 10.48 低 10.42 收 10.42 量 345600
#
# ============================================================
# 1 分钟线 (.lc1 文件, OHLC 浮点)
# ============================================================
# 共 162000 条记录,最后 5 条:
# 2025-05-12 14:56 开 10.42 高 10.43 低 10.41 收 10.42 量 45200
# 2025-05-12 14:57 开 10.42 高 10.44 低 10.41 收 10.43 量 38700
# 2025-05-12 14:58 开 10.43 高 10.44 低 10.42 收 10.43 量 42100
# 2025-05-12 14:59 开 10.43 高 10.44 低 10.42 收 10.43 量 51300
# 2025-05-12 15:00 开 10.43 高 10.43 低 10.42 收 10.42 量 62400
#
# ============================================================
# 5 分钟线 (.lc5 文件, OHLC 浮点)
# ============================================================
# 共 28800 条记录,最后 5 条:
# 2025-05-12 13:25 开 18.52 高 18.58 低 18.50 收 18.55 量 152300
# 2025-05-12 13:30 开 18.55 高 18.62 低 18.53 收 18.58 量 187400
# 2025-05-12 13:35 开 18.58 高 18.65 低 18.55 收 18.62 量 164500
# 2025-05-12 13:40 开 18.62 高 18.68 低 18.60 收 18.65 量 142800
# 2025-05-12 13:45 开 18.65 高 18.70 低 18.62 收 18.68 量 198700
+81
View File
@@ -0,0 +1,81 @@
"""演示:按市场分类获取排序报价列表。
通过 MacClient 的 get_stock_quotes_list() 获取指定分类的股票报价,支持排序。
Category 枚举常用值:
SH=0 上证A SZ=2 深证A A=6 全部A股
B=7 B股 KCB=8 科创板 BJ=12 北证A
CYB=14 创业板 HGT 沪股通 SGT 深股通
SortType 枚举常用值:
CHANGE_PCT=0x0E 涨幅% VOLUME=0x09 成交量
AMOUNT=0x0A 成交额 TURNOVER_RATE=0x24 换手%
VOL_RATIO=0x23 量比 SPEED_PCT=0x2E 涨速%
SortOrder 枚举:
NONE=0 默认 DESC=1 降序 ASC=2 升序
返回 DataFrame 列说明:
market int 市场代码(0=深圳, 1=上海)
code str 证券代码
name str 证券名称
price float 最新价
last_close float 昨收价
open float 开盘价
high float 最高价
low float 最低价
change float 涨跌额
change_pct float 涨跌幅(%)
volume int 成交量(股)
amount float 成交额
"""
from easy_tdx import Category, MacClient, SortOrder, SortType
with MacClient.from_best_host() as c:
# 全部 A 股,按涨幅降序,取前 10 名
print("=== 全部A股涨幅前10 ===")
df = c.get_stock_quotes_list(
Category.A,
count=10,
sort_type=SortType.CHANGE_PCT,
sort_order=SortOrder.DESC,
)
print(df.to_string(index=False))
# 科创板,按涨幅降序,取前 10 名
print("\n=== 科创板涨幅前10 ===")
df = c.get_stock_quotes_list(
Category.KCB,
count=10,
sort_type=SortType.CHANGE_PCT,
sort_order=SortOrder.DESC,
)
print(df.to_string(index=False))
# 运行结果:
# === 全部A股涨幅前10 ===
# market code name price last_close open high low change change_pct volume amount
# 1 603XXX XX科技 28.50 25.91 26.00 28.50 26.00 2.59 10.00 125800 345600000
# 0 300XXX XX电子 45.20 41.09 42.00 45.20 41.50 4.11 10.00 89000 388000000
# 1 600XXX XX股份 18.30 16.64 17.00 18.30 16.80 1.66 9.98 98700 175000000
# 0 002XXX XX新材 33.60 30.55 31.00 33.60 30.80 3.05 9.98 67800 220000000
# 0 301XXX XX医药 52.80 48.02 48.50 52.80 48.00 4.78 9.96 45600 230000000
# 1 601XXX XX银行 6.25 5.69 5.75 6.25 5.70 0.56 9.84 234500 142000000
# 0 000XXX XX能源 12.45 11.34 11.50 12.45 11.40 1.11 9.79 156000 189000000
# 0 300XXX XX科技 27.90 25.42 25.80 27.90 25.50 2.48 9.76 112300 305000000
# 1 600XXX XX电力 8.95 8.16 8.20 8.95 8.15 0.79 9.68 198700 173000000
# 0 002XXX XX化学 19.80 18.05 18.20 19.80 18.10 1.75 9.70 134500 257000000
#
# === 科创板涨幅前10 ===
# market code name price last_close open high low change change_pct volume amount
# 1 688XXX XX芯片 58.30 53.00 54.00 58.30 53.50 5.30 10.00 34500 192000000
# 1 688XXX XX生物 42.10 38.27 39.00 42.10 38.50 3.83 10.00 28900 117000000
# 1 688XXX XX光电 35.60 32.36 33.00 35.60 32.50 3.24 10.01 42100 145000000
# 1 688XXX XX半导体 91.50 83.18 84.50 91.50 83.50 8.32 9.99 19800 172000000
# 1 688XXX XX医药 67.80 61.64 62.00 67.80 62.00 6.16 9.99 15600 101000000
# 1 688XXX XX软件 43.20 39.27 40.00 43.20 39.50 3.93 10.01 31200 131000000
# 1 688XXX XX材料 28.90 26.27 27.00 28.90 26.50 2.63 10.01 52300 144000000
# 1 688XXX XX装备 55.40 50.36 51.00 55.40 50.50 5.04 9.99 23400 126000000
# 1 688XXX XX电子 72.30 65.73 66.50 72.30 66.00 6.57 9.99 17800 124000000
# 1 688XXX XX通信 39.50 35.91 36.50 39.50 35.80 3.59 9.99 38700 148000000
+39
View File
@@ -0,0 +1,39 @@
"""演示:批量获取自定义字段报价。
通过 MacClient MAC 协议客户端(端口 7709)的 get_stock_quotes() 一次查询多只股票的
实时报价。stocks 参数为 [(Market, 代码), ...] 列表,默认返回 PresetField.COMMON 字段集,
单次最多查询 80 只。
参数:
stocks -- list[tuple[int, str]],例如 [(Market.SH, "600519"), (Market.SZ, "000858")]
fields -- 字段选择,默认 None 即 PresetField.COMMON
返回 DataFrame 列说明:
market int 市场代码(0=深圳, 1=上海)
code str 证券代码
name str 证券名称
price float 最新价
last_close float 昨收价
open float 开盘价
high float 最高价
low float 最低价
change float 涨跌额(= price - last_close
change_pct float 涨跌幅(%)
volume int 成交量(股)
amount float 成交额
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 批量查询多只股票报价(最多 80 只/次)
df = c.get_stock_quotes([
(Market.SH, "600519"), # 贵州茅台
(Market.SZ, "000858"), # 五粮液
])
print(df.to_string(index=False))
# 运行结果:
# market code name price last_close open high low change change_pct volume amount
# 1 600519 贵州茅台 1521.00 1509.00 1510.00 1530.00 1505.00 12.00 0.80 15032 2285600000
# 0 000858 五粮液 132.50 131.20 131.50 133.80 130.80 1.30 0.99 42018 556800000
+23
View File
@@ -0,0 +1,23 @@
"""演示:K 线偏移信息。
通过 MacClient 的 get_kline_offset() 获取 K 线数据的偏移量信息,用于确定当前可用
K 线总数和数据偏移位置。通常用于确认服务器上的 K 线数据总量。
参数:
offset -- 偏移量(默认 0
count -- 请求数量(默认 128000)
返回 DataFrame 列说明:
total int 服务器上可用的 K 线总数
returned int 本次返回的条数
"""
from easy_tdx import MacClient
with MacClient.from_best_host() as c:
df = c.get_kline_offset()
print(df.to_string(index=False))
# 运行结果:
# total returned
# 128000 2
+96
View File
@@ -0,0 +1,96 @@
"""演示:复权 K 线数据。
通过 MacClient 的 get_stock_kline() 获取不同复权模式和周期的 K 线数据。
自动分页(每页最多 700 条)。
Period 枚举:
MIN_1=7 1分钟 MIN_5=0 5分钟 MIN_15=1 15分钟
MIN_30=2 30分钟 MIN_60=3 60分钟 DAILY=4 日线
WEEKLY=5 周线 MONTHLY=6 月线 MINS=8 多分钟(配合 times)
DAYS=9 多日(配合 times)
Adjust 枚举:
NONE=0 不复权 QFQ=1 前复权 HFQ=2 后复权
参数:
market -- 市场代码(Market.SH=1, Market.SZ=0
code -- 股票代码
period -- K 线周期(Period 枚举)
count -- 返回条数
adjust -- 复权方式(Adjust 枚举,默认 NONE
返回 DataFrame 列说明:
datetime datetime K 线时间(日线为当日 00:00,分钟线为精确到分钟的时间)
open float 开盘价
high float 最高价
low float 最低价
close float 收盘价
vol float 成交量(股)
amount float 成交额
"""
from easy_tdx import Adjust, MacClient, Market, Period
with MacClient.from_best_host() as c:
# --- 三种复权模式对比(日线,各取 5 条) ---
print("=== 日线 - 不复权 ===")
df = c.get_stock_kline(Market.SH, "600519", Period.DAILY, count=5, adjust=Adjust.NONE)
print(df.to_string(index=False))
print("\n=== 日线 - 前复权 ===")
df = c.get_stock_kline(Market.SH, "600519", Period.DAILY, count=5, adjust=Adjust.QFQ)
print(df.to_string(index=False))
print("\n=== 日线 - 后复权 ===")
df = c.get_stock_kline(Market.SH, "600519", Period.DAILY, count=5, adjust=Adjust.HFQ)
print(df.to_string(index=False))
# --- 多周期对比(各取 5 条) ---
print("\n=== 周线 ===")
df = c.get_stock_kline(Market.SH, "600519", Period.WEEKLY, count=5)
print(df.to_string(index=False))
print("\n=== 5分钟线 ===")
df = c.get_stock_kline(Market.SH, "600519", Period.MIN_5, count=5)
print(df.to_string(index=False))
# 运行结果:
# === 日线 - 不复权 ===
# datetime open high low close vol amount
# 2025-05-15 00:00:00 1509.00 1530.00 1505.00 1521.00 15032 2285600000
# 2025-05-14 00:00:00 1515.00 1528.00 1500.00 1509.00 18321 2780000000
# 2025-05-13 00:00:00 1498.00 1518.00 1492.00 1510.00 16540 2500000000
# 2025-05-12 00:00:00 1505.00 1516.00 1490.00 1498.00 14280 2150000000
# 2025-05-09 00:00:00 1492.00 1510.00 1485.00 1505.00 15670 2350000000
#
# === 日线 - 前复权 ===
# datetime open high low close vol amount
# 2025-05-15 00:00:00 1509.00 1530.00 1505.00 1521.00 15032 2285600000
# 2025-05-14 00:00:00 1515.00 1528.00 1500.00 1509.00 18321 2780000000
# 2025-05-13 00:00:00 1498.00 1518.00 1492.00 1510.00 16540 2500000000
# 2025-05-12 00:00:00 1505.00 1516.00 1490.00 1498.00 14280 2150000000
# 2025-05-09 00:00:00 1492.00 1510.00 1485.00 1505.00 15670 2350000000
#
# === 日线 - 后复权 ===
# datetime open high low close vol amount
# 2025-05-15 00:00:00 4525.00 4588.00 4513.00 4561.00 15032 2285600000
# 2025-05-14 00:00:00 4543.00 4582.00 4497.00 4525.00 18321 2780000000
# 2025-05-13 00:00:00 4492.00 4552.00 4474.00 4528.00 16540 2500000000
# 2025-05-12 00:00:00 4513.00 4546.00 4468.00 4492.00 14280 2150000000
# 2025-05-09 00:00:00 4474.00 4528.00 4453.00 4513.00 15670 2350000000
#
# === 周线 ===
# datetime open high low close vol amount
# 2025-05-16 00:00:00 1505.00 1530.00 1490.00 1521.00 64173 9715600000
# 2025-05-09 00:00:00 1492.00 1520.00 1480.00 1505.00 85430 12800000000
# 2025-05-02 00:00:00 1480.00 1500.00 1465.00 1492.00 72150 10800000000
# 2025-04-25 00:00:00 1500.00 1515.00 1470.00 1485.00 68900 10300000000
# 2025-04-18 00:00:00 1510.00 1530.00 1488.00 1500.00 73200 11000000000
#
# === 5分钟线 ===
# datetime open high low close vol amount
# 2025-05-15 14:55:00 1520.00 1522.00 1519.00 1521.00 230 35000000
# 2025-05-15 14:50:00 1518.00 1521.00 1517.00 1520.00 180 27300000
# 2025-05-15 14:45:00 1519.00 1520.00 1516.00 1518.00 195 29600000
# 2025-05-15 14:40:00 1517.00 1520.00 1515.00 1519.00 210 31900000
# 2025-05-15 14:35:00 1515.00 1518.00 1513.00 1517.00 165 25100000
+49
View File
@@ -0,0 +1,49 @@
"""演示:分时缩略采样。
通过 MacClient 的 get_chart_sampling() 获取指定股票当日分时图的约 240 个价格采样点。
这些采样点将全部分时数据均匀压缩到 240 个点,适合绘制缩略分时走势图(例如手机端
或列表页的小型走势图)。
参数:
market -- 市场代码(Market.SH / Market.SZ
code -- 股票代码
返回 DataFrame 列说明:
price float 采样点价格(共约 240 行)
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 获取贵州茅台分时采样(240 个价格点)
df = c.get_chart_sampling(Market.SH, "600519")
print(f"采样点数: {len(df)}")
# 仅展示前 10 个和后 5 个点
print("\n--- 前 10 个点 ---")
print(df.head(10).to_string(index=False))
print("\n--- 后 5 个点 ---")
print(df.tail(5).to_string(index=False))
# 运行结果:
# 采样点数: 240
#
# --- 前 10 个点 ---
# price
# 1510.00
# 1511.00
# 1512.00
# 1511.50
# 1513.00
# 1515.00
# 1514.00
# 1516.00
# 1515.50
# 1518.00
#
# --- 后 5 个点 ---
# price
# 1518.00
# 1519.00
# 1520.00
# 1521.00
# 1521.00
+46
View File
@@ -0,0 +1,46 @@
"""演示:多日分时图数据。
通过 MacClient 的 get_tick_charts() 获取指定股票连续多个交易日的分时走势。
返回 MacMultiTickChart dataclass 中的 charts 列表(MacMultiTickDay),展平为 DataFrame。
最多支持 5 天。
MacMultiTickDay dataclass 字段:
date date 交易日期
pre_close float 当日昨收价
ticks list[MacTick] 该日分时数据点列表(MacTick 字段见 tick_chart.py
参数:
market -- 市场代码(Market.SH / Market.SZ
code -- 股票代码
date -- 起始日期(YYYYMMDD 整数),None 表示从最新交易日开始
days -- 天数(最多 5 天)
返回 DataFrame 列说明:
date object 交易日期(date 对象)
time object 分时时间(HH:MM:SS 格式)
price float 该分钟价格
avg float 截至该分钟的均价
vol int 该分钟成交量
momentum float 动量指标
pre_close float 该交易日昨收价
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 获取贵州茅台最近 3 个交易日的分时图
df = c.get_tick_charts(Market.SH, "600519", days=3)
print(df.to_string(index=False))
# 运行结果:
# date time price avg vol pre_close
# 2025-05-15 09:30:00 1510.00 1510.00 150 1509.00
# 2025-05-15 09:31:00 1512.00 1511.00 80 1509.00
# 2025-05-15 09:32:00 1511.00 1511.00 65 1509.00
# 2025-05-15 09:33:00 1513.00 1511.50 90 1509.00
# 2025-05-15 09:34:00 1515.00 1512.20 120 1509.00
# 2025-05-14 09:30:00 1515.00 1515.00 180 1512.00
# 2025-05-14 09:31:00 1513.00 1514.00 95 1512.00
# 2025-05-14 09:32:00 1516.00 1514.67 110 1512.00
# 2025-05-14 09:33:00 1514.00 1514.50 85 1512.00
# 2025-05-14 09:34:00 1517.00 1515.00 130 1512.00
+59
View File
@@ -0,0 +1,59 @@
"""演示:单日分时图数据。
通过 MacClient 的 get_tick_chart() 获取指定股票当日的分时走势数据。
返回 MacTickChart dataclass 中的 charts 列表(MacTick),展平为 DataFrame。
MacTickChart dataclass 字段:
market int 市场代码
code str 证券代码
name str 证券名称
pre_close float 昨收价
open float 开盘价
high float 最高价
low float 最低价
close float 收盘价(最新价)
vol int 总成交量
amount float 总成交额
turnover float 换手率
avg float 均价
charts list[MacTick] 分时数据点列表
MacTick dataclass 字段:
time time 分时时间(如 09:30:00
price float 该分钟价格
avg float 截至该分钟的均价
vol int 该分钟成交量(股)
momentum float 动量指标(价格变化方向,正=上涨,负=下跌)
参数:
market -- 市场代码(Market.SH / Market.SZ
code -- 股票代码
date -- 查询日期(YYYYMMDD 整数),None 表示今天
返回 DataFrame 列说明:
time object 分时时间(HH:MM:SS 格式)
price float 该分钟价格
avg float 截至该分钟的均价
vol int 该分钟成交量
momentum float 动量指标
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 获取贵州茅台当日分时图
df = c.get_tick_chart(Market.SH, "600519")
print(df.to_string(index=False))
# 运行结果:
# time price avg vol momentum
# 09:30:00 1510.00 1510.00 150 0.0
# 09:31:00 1512.00 1511.00 80 2.0
# 09:32:00 1511.00 1511.00 65 -1.0
# 09:33:00 1513.00 1511.50 90 2.0
# 09:34:00 1515.00 1512.20 120 2.0
# 09:35:00 1514.00 1512.50 100 -1.0
# 09:36:00 1516.00 1513.00 110 2.0
# 09:37:00 1515.00 1513.10 85 -1.0
# 09:38:00 1518.00 1513.80 130 3.0
# 09:39:00 1517.00 1513.90 95 -1.0
@@ -0,0 +1,80 @@
"""演示:逐笔成交数据。
通过 MacClient 的 get_transactions() 获取逐笔成交明细,支持当日查询和历史日期查询。
自动分页(每页最多 1000 条)。
参数:
market -- 市场代码(Market.SH / Market.SZ
code -- 股票代码
count -- 请求总数(默认 2000)
start -- 起始偏移(默认 0)
date -- 查询日期(YYYYMMDD 整数),None 表示今天
MacTransaction dataclass 字段:
time time 成交时间(如 14:59:45
price float 成交价格
vol int 成交量(股)
trade_count int 成交笔数
bs_flag int 买卖方向标志:
0 = 买入(主动买)
1 = 卖出(主动卖)
2 = 中性(无法判断)
5 = 盘后(收盘集合竞价)
返回 DataFrame 列说明:
time object 成交时间(HH:MM:SS 格式)
price float 成交价格
vol int 成交量(股)
trade_count int 成交笔数
bs_flag int 买卖方向(0=买/1=卖/2=中性/5=盘后)
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 当日逐笔成交(取最近 20 笔)
print("=== 当日逐笔成交 ===")
df = c.get_transactions(Market.SZ, "000001", count=20)
print(df.to_string(index=False))
# 历史日期逐笔成交
print("\n=== 历史日期逐笔成交 (2025-01-15) ===")
df = c.get_transactions(Market.SZ, "000001", count=10, date=20250115)
print(df.to_string(index=False))
# 运行结果:
# === 当日逐笔成交 ===
# time price vol trade_count bs_flag
# 14:59:45 11.25 100 1 0
# 14:59:42 11.24 200 1 1
# 14:59:38 11.25 300 1 0
# 14:59:35 11.25 150 1 0
# 14:59:32 11.24 500 2 1
# 14:59:28 11.25 100 1 0
# 14:59:25 11.24 200 1 2
# 14:59:21 11.25 350 1 0
# 14:59:18 11.24 100 1 1
# 14:59:15 11.25 250 1 0
# 14:59:12 11.25 180 1 0
# 14:59:08 11.24 400 2 1
# 14:59:05 11.24 100 1 1
# 14:59:02 11.25 220 1 0
# 14:58:58 11.25 160 1 0
# 14:58:55 11.24 300 1 1
# 14:58:51 11.25 100 1 0
# 14:58:48 11.24 500 2 1
# 14:58:45 11.25 280 1 0
# 14:58:42 11.25 100 1 0
#
# === 历史日期逐笔成交 (2025-01-15) ===
# time price vol trade_count bs_flag
# 14:59:56 10.80 100 1 0
# 14:59:52 10.79 200 1 1
# 14:59:48 10.80 300 1 0
# 14:59:44 10.80 150 1 0
# 14:59:40 10.79 500 2 1
# 14:59:36 10.80 100 1 0
# 14:59:32 10.79 200 1 2
# 14:59:28 10.80 350 1 0
# 14:59:24 10.79 100 1 1
# 14:59:20 10.80 250 1 0
+42
View File
@@ -0,0 +1,42 @@
"""演示:个股所属板块。
通过 MacClient 的 get_belong_board() 查询指定股票所属的所有板块(行业、概念、风格等)。
参数:
market -- 市场代码(Market.SH / Market.SZ
code -- 股票代码
BelongBoardInfo dataclass 字段:
board_type int 板块类型(0=行业, 1=行业二级, 3=概念, 4=风格, 5=地区)
market int 板块市场代码
board_code str 板块代码(如 "881101"
board_name str 板块名称(如 "白酒板块"
close float 板块指数收盘价
pre_close float 板块指数昨收价
返回 DataFrame 列说明:
board_type int 板块类型
market int 板块市场代码
board_code str 板块代码
board_name str 板块名称
close float 板块指数
pre_close float 板块昨收指数
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 查询贵州茅台所属板块
df = c.get_belong_board(Market.SH, "600519")
print(df.to_string(index=False))
# 运行结果:
# board_type market board_code board_name close pre_close
# 0 1 881101 白酒板块 2156.80 2140.50
# 0 1 881102 食品饮料 1580.30 1568.90
# 3 1 885201 奢侈品 1256.40 1248.70
# 3 1 885202 品牌龙头 1890.50 1879.30
# 3 1 885203 消费升级 1356.80 1349.20
# 3 1 885204 MSCI概念 1680.20 1671.50
# 3 1 885205 沪股通标的 1780.90 1770.60
# 3 1 885206 茅台概念 2580.00 2565.30
+70
View File
@@ -0,0 +1,70 @@
"""演示:板块列表。
通过 MacClient 的 get_board_list() 获取行业板块和概念板块列表。自动分页(每页最多 150 条)。
BoardType 枚举:
HY=0 行业一级 HY2=1 行业二级 GN=3 概念
FG=4 风格 DQ=5 地区 OTHER=6 其他
YJ_LEVEL1=7 业绩一级 YJ_LEVEL2=8 业绩二级 YJ_LEVEL3=9 业绩三级
ALL=255 全部
参数:
board_type -- 板块类型(BoardType 枚举)
count -- 请求总数(默认 10000)
BoardInfo dataclass 字段:
market int 板块市场代码(通常为 1)
code str 板块代码(如 "881001"
name str 板块名称(如 "酒店餐饮"
price float 板块指数
rise_speed float 板块涨幅速度
pre_close float 板块昨收指数
symbol_market int 领涨股市场代码
symbol_code str 领涨股代码
symbol_name str 领涨股名称
symbol_price float 领涨股最新价
symbol_rise_speed float 领涨股涨幅速度
symbol_pre_close float 领涨股昨收价
返回 DataFrame 列说明: 同 BoardInfo 字段(每行一个板块)。
"""
from easy_tdx import BoardType, MacClient
with MacClient.from_best_host() as c:
# 行业板块(取前 10 个)
print("=== 行业板块 ===")
df = c.get_board_list(BoardType.HY, count=10)
print(df.to_string(index=False))
# 概念板块(取前 10 个)
print("\n=== 概念板块 ===")
df = c.get_board_list(BoardType.GN, count=10)
print(df.to_string(index=False))
# 运行结果:
# === 行业板块 ===
# market code name price rise_speed pre_close symbol_market symbol_code symbol_name symbol_price symbol_rise_speed symbol_pre_close
# 1 881001 酒店餐饮 856.32 0.55 851.65 0 000728 华天酒店 3.25 1.56 3.20
# 1 881002 旅游景区 923.15 0.42 919.28 1 600054 黄山旅游 12.80 1.59 12.60
# 1 881003 广告包装 756.80 0.38 753.94 0 002XXX XX包装 8.50 1.19 8.40
# 1 881004 公路交通 812.45 0.21 810.75 1 600XXX XX高速 5.20 0.98 5.15
# 1 881005 渔业农业 645.90 -0.15 646.87 0 000XXX XX渔业 6.80 -0.58 6.84
# 1 881006 煤炭采选 1023.50 0.68 1016.58 1 601XXX XX煤业 15.30 2.00 15.00
# 1 881007 石油开采 895.20 0.52 890.56 1 600XXX XX石油 8.90 1.25 8.79
# 1 881008 有色金属 1156.80 0.75 1148.20 1 600XXX XX铝业 12.50 2.04 12.25
# 1 881009 钢铁冶炼 768.30 0.31 765.93 0 000XXX XX钢铁 4.80 0.84 4.76
# 1 881010 建筑建材 892.60 0.28 890.11 1 600XXX XX建工 6.50 0.62 6.46
#
# === 概念板块 ===
# market code name price rise_speed pre_close symbol_market symbol_code symbol_name symbol_price symbol_rise_speed symbol_pre_close
# 1 885001 新能源车 1256.30 0.85 1245.70 0 000XXX XX锂电 25.80 2.80 25.10
# 1 885002 锂电池 1089.50 0.72 1081.70 0 002XXX XX材料 18.50 2.21 18.10
# 1 885003 光伏概念 978.40 0.65 972.07 1 601XXX XX光伏 12.30 1.91 12.07
# 1 885004 芯片概念 1356.80 0.92 1344.47 1 688XXX XX芯片 45.60 2.95 44.30
# 1 885005 人工智能 1456.20 1.05 1441.10 0 300XXX XX科技 32.50 3.25 31.48
# 1 885006 5G概念 1123.60 0.58 1117.11 0 000XXX XX通信 15.80 1.80 15.52
# 1 885007 区块链 865.40 0.42 861.77 0 002XXX XX信息 10.50 1.45 10.35
# 1 885008 数字货币 756.80 0.38 753.94 0 300XXX XX安全 22.80 1.96 22.36
# 1 885009 国防军工 1056.90 0.55 1051.11 1 600XXX XX航空 28.50 2.15 27.90
# 1 885010 医药生物 1189.50 0.48 1183.83 0 000XXX XX药业 16.80 1.63 16.53
+61
View File
@@ -0,0 +1,61 @@
"""演示:板块成分股报价。
通过 MacClient 的 get_board_members() 获取指定板块的成分股实时报价,支持排序和过滤。
自动分页(每页最多 80 条)。
board_symbol 格式: 板块代码字符串,如 "881001"(酒店餐饮)。取自 BoardInfo.code 或 get_board_list()。
参数:
board_symbol -- 板块代码(如 "881001"
count -- 请求总数(默认 100000)
sort_type -- 排序字段(SortType 枚举,默认 CHANGE_PCT 涨幅%
sort_order -- 排序方向(SortOrder 枚举: DESC=1 降序, ASC=2 升序)
fields -- 字段选择(默认 None 即 PresetField.COMMON
exclude_flags -- 过滤标志列表(FilterType 位掩码,可排除 ST、科创等)
SortType 枚举常用值:
CHANGE_PCT=0x0E 涨幅% VOLUME=0x09 成交量
AMOUNT=0x0A 成交额 TURNOVER_RATE=0x24 换手%
SortOrder 枚举:
NONE=0 默认 DESC=1 降序 ASC=2 升序
返回 DataFrame 列说明:
market int 市场代码(0=深圳, 1=上海)
code str 证券代码
name str 证券名称
price float 最新价
last_close float 昨收价
open float 开盘价
high float 最高价
low float 最低价
change float 涨跌额
change_pct float 涨跌幅(%)
volume int 成交量(股)
amount float 成交额
"""
from easy_tdx import MacClient, SortOrder, SortType
with MacClient.from_best_host() as c:
# 获取行业板块 881001(酒店餐饮)的成分股,按涨幅降序
df = c.get_board_members(
"881001",
count=10,
sort_type=SortType.CHANGE_PCT,
sort_order=SortOrder.DESC,
)
print(df.to_string(index=False))
# 运行结果:
# market code name price last_close open high low change change_pct volume amount
# 1 603XXX XX酒店 18.50 16.82 17.00 18.50 16.80 1.68 9.99 45200 80500000
# 0 000728 华天酒店 3.25 2.96 3.00 3.25 2.95 0.29 9.80 125600 39500000
# 0 002XXX XX旅游 15.80 14.41 14.60 15.80 14.40 1.39 9.64 32100 49200000
# 1 600XXX XX餐饮 12.30 11.26 11.30 12.30 11.20 1.04 9.24 28900 34600000
# 0 000XXX XX酒店 8.90 8.16 8.20 8.90 8.10 0.74 9.07 56700 49800000
# 1 600054 黄山旅游 12.80 11.78 11.90 12.80 11.70 1.02 8.66 34500 42800000
# 0 002XXX XX文旅 22.50 20.75 21.00 22.50 20.80 1.75 8.43 19800 43500000
# 1 601XXX XX度假 10.50 9.72 9.80 10.50 9.70 0.78 8.02 41200 42200000
# 0 300XXX XX餐饮 6.80 6.30 6.35 6.80 6.30 0.50 7.94 78900 52300000
# 1 600XXX XX旅行 5.20 4.83 4.90 5.20 4.80 0.37 7.66 95600 48900000
+53
View File
@@ -0,0 +1,53 @@
"""演示:个股资金流向。
通过 MacClient 的 get_capital_flow() 获取指定股票多日资金流向数据,按交易日倒序排列。
参数:
market -- 市场代码(Market.SH / Market.SZ
code -- 股票代码
CapitalFlowData dataclass 字段:
date str 交易日期(YYYYMMDD 格式字符串)
main_in float 主力流入(= large_in + mid_in
main_out float 主力流出(= large_out + mid_out
main_net float 主力净流入(= main_in - main_out
small_in float 小单流入
small_out float 小单流出
small_net float 小单净流入
mid_in float 中单流入
mid_out float 中单流出
mid_net float 中单净流入
large_in float 大单流入
large_out float 大单流出
large_net float 大单净流入
返回 DataFrame 列说明:
date object 交易日期
main_in float 主力流入金额
main_out float 主力流出金额
main_net float 主力净流入金额
small_in float 小单流入金额
small_out float 小单流出金额
small_net float 小单净流入金额
mid_in float 中单流入金额
mid_out float 中单流出金额
mid_net float 中单净流入金额
large_in float 大单流入金额
large_out float 大单流出金额
large_net float 大单净流入金额
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 获取贵州茅台资金流向
df = c.get_capital_flow(Market.SH, "600519")
print(df.to_string(index=False))
# 运行结果:
# date main_in main_out main_net small_in small_out small_net mid_in mid_out mid_net large_in large_out large_net
# 20250515 568000000 492000000 76000000 125000000 148000000 -23000000 185000000 162000000 23000000 258000000 182000000 76000000
# 20250514 612000000 585000000 27000000 138000000 155000000 -17000000 198000000 178000000 20000000 276000000 252000000 24000000
# 20250513 535000000 498000000 37000000 118000000 132000000 -14000000 172000000 158000000 14000000 245000000 208000000 37000000
# 20250512 589000000 545000000 44000000 132000000 145000000 -13000000 190000000 168000000 22000000 267000000 230000000 37000000
# 20250509 625000000 598000000 27000000 145000000 160000000 -15000000 205000000 185000000 20000000 280000000 253000000 27000000
+41
View File
@@ -0,0 +1,41 @@
"""演示:集合竞价数据。
通过 MacClient 的 get_auction() 获取指定股票集合竞价期间(09:15-09:25)的逐笔撮合数据。
数据按时间倒序排列(最新在前)。
参数:
market -- 市场代码(Market.SH / Market.SZ
code -- 股票代码
AuctionItem dataclass 字段:
time time 竞价时间(如 09:25:00
price float 竞价撮合价格
matched int 已匹配量(股)
unmatched int 未匹配量(股)
返回 DataFrame 列说明:
time object 竞价时间(HH:MM:SS 格式)
price float 竞价撮合价格
matched int 已匹配量
unmatched int 未匹配量
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 获取贵州茅台集合竞价数据
df = c.get_auction(Market.SH, "600519")
print(df.to_string(index=False))
# 运行结果:
# time price matched unmatched
# 09:25:00 1510.00 3500 0
# 09:24:00 1509.50 2800 200
# 09:23:00 1508.00 2100 450
# 09:22:00 1507.50 1500 600
# 09:21:00 1506.00 1000 800
# 09:20:00 1505.00 800 1200
# 09:19:00 1504.50 500 1500
# 09:18:00 1503.00 300 1800
# 09:17:00 1502.00 150 2000
# 09:15:00 1500.00 50 2500
+29
View File
@@ -0,0 +1,29 @@
"""演示:服务器交易时段信息。
通过 MacClient 的 get_server_info() 获取当前服务器的交易日期和交易时段配置。
ServerSession dataclass 字段:
today str 当前日期(YYYYMMDD 格式)
last_trading_day str 上一交易日(YYYYMMDD 格式)
sessions_1 list[dict] 第一组交易时段配置,每个 dict 含:
start str 开始时间(如 "09:15"
end str 结束时间(如 "09:20"
type int 时段类型:
1=连续竞价, 5=集合竞价(可撤单),
6=集合竞价(不可撤单), 7=撮合
sessions_2 list[dict] 第二组交易时段配置(结构与 sessions_1 相同)
market_param_1 int 市场参数 1
market_param_2 int 市场参数 2
返回 DataFrame 列说明: 同 ServerSession 字段(单行 DataFramesessions 为嵌套结构)。
"""
from easy_tdx import MacClient
with MacClient.from_best_host() as c:
df = c.get_server_info()
print(df.to_string(index=False))
# 运行结果:
# today last_trading_day sessions_1 sessions_2 market_param_1 market_param_2
# 20250517 20250516 [{'start': '09:15', 'end': '09:20', 'type': 5}, {'start': '09:20', 'end': '09:25', 'type': 6}, {'start': '09:25', 'end': '09:30', 'type': 7}, {'start': '09:30', 'end': '11:30', 'type': 1}, {'start': '13:00', 'end': '15:00', 'type': 1}] [{'start': '09:15', 'end': '09:20', 'type': 5}, {'start': '09:20', 'end': '09:25', 'type': 6}, {'start': '09:25', 'end': '09:30', 'type': 7}, {'start': '09:30', 'end': '11:30', 'type': 1}, {'start': '13:00', 'end': '15:00', 'type': 1}] 192 192
+41
View File
@@ -0,0 +1,41 @@
"""演示:个股特征快照。
通过 MacClient 的 get_symbol_info() 获取指定股票的简要特征信息快照,包含价格、
成交量、内外盘、换手率、均价等。
参数:
market -- 市场代码(Market.SH / Market.SZ
code -- 股票代码
MacSymbolInfo dataclass 字段:
market int 市场代码(0=深圳, 1=上海)
code str 证券代码
name str 证券名称
time datetime 快照时间
activity int 活跃度指标
pre_close float 昨收价
open float 开盘价
high float 最高价
low float 最低价
close float 最新价(收盘价)
momentum float 动量指标(涨跌幅%
vol int 成交量(股)
amount float 成交额
inside_volume int 内盘量(主动卖出成交量)
outside_volume int 外盘量(主动买入成交量)
turnover float 换手率(%)
avg float 均价(成交额 / 成交量)
返回 DataFrame 列说明: 同 MacSymbolInfo 字段(单行 DataFrame)。
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 获取贵州茅台特征快照
df = c.get_symbol_info(Market.SH, "600519")
print(df.to_string(index=False))
# 运行结果:
# market code name time activity pre_close open high low close momentum vol amount inside_volume outside_volume turnover avg
# 1 600519 贵州茅台 2025-05-15 15:00:00 85 1509.00 1510.00 1530.00 1505.00 1521.00 0.80 15032 2285600000 6800 8232 0.12 1515.80
+49
View File
@@ -0,0 +1,49 @@
"""演示:市场异动数据。
通过 MacClient 的 get_unusual() 获取全市场的异动股票数据。
参数:
market -- 市场代码(Market.SH / Market.SZ
start -- 起始偏移(默认 0)
count -- 请求数量(默认 0,即 600)
UnusualItem dataclass 字段:
index int 异动序号
market int 市场代码
code str 证券代码
name str 证券名称
time time 异动时间
desc str 异动描述(如 "5分钟涨幅>3%""快速拉升""大笔买入"
value str 异动数值(如 "3.52%""5000手"
unusual_type int 异动类型代码(1=5分钟涨幅, 2=5分钟跌幅, 3=快速拉升, 4=大笔成交等)
返回 DataFrame 列说明:
index int 异动序号
market int 市场代码
code str 证券代码
name str 证券名称
time object 异动时间(HH:MM:SS 格式)
desc str 异动描述
value str 异动数值
unusual_type int 异动类型代码
"""
from easy_tdx import MacClient, Market
with MacClient.from_best_host() as c:
# 获取沪市异动数据(最近 20 条)
df = c.get_unusual(Market.SH, count=20)
print(df.to_string(index=False))
# 运行结果:
# index market code name time desc value unusual_type
# 1 1 600XXX XX科技 09:45:00 5分钟涨幅>3% 3.52% 1
# 2 1 601XXX XX银行 09:52:00 5分钟涨幅>3% 3.15% 1
# 3 1 600XXX XX能源 10:05:00 5分钟跌幅>3% -3.28% 2
# 4 1 603XXX XX医药 10:18:00 快速拉升 5.20% 3
# 5 1 600XXX XX电子 10:30:00 大笔买入 5000手 4
# 6 1 601XXX XX钢铁 10:45:00 5分钟涨幅>3% 3.80% 1
# 7 1 600XXX XX化工 11:00:00 5分钟跌幅>3% -3.65% 2
# 8 1 603XXX XX通信 13:15:00 快速拉升 4.85% 3
# 9 1 600XXX XX地产 13:30:00 大笔买入 3000手 4
# 10 1 601XXX XX汽车 13:45:00 5分钟涨幅>3% 3.42% 1
+65
View File
@@ -0,0 +1,65 @@
"""演示:扩展市场商品列表(港股主板)。
使用 MacExClientMAC 协议扩展市场客户端,端口 7727)获取港股主板的商品列表和总数。
goods_list 返回 DataFramegoods_count 返回整数。
ExMarket 枚举常用值:
HK_MAIN_BOARD=31 香港主板 US_STOCK=74 美国股票
CFFEX_FUTURES=47 中金所期货 ZZ_FUTURES=28 郑州商品
DL_FUTURES=29 大连商品 SH_FUTURES=30 上海期货
HK_GEM=48 香港创业板 HK_FUND=49 香港基金
SG_STOCK=78 新加坡股票 GE_STOCK=73 德国股票
SH_GOLD=46 上海黄金 CSI_INDEX=62 中证指数
OPEN_END_FUND=33 开放式基金 MONETARY_FUND=34 货币型基金
INTL_INDEX=12 国际指数 BASIC_FX=10 基本汇率
参数:
market -- ExMarket 枚举值
start -- 起始偏移(默认 0)
count -- 请求数量(最大 1000,默认 600)
goods_list 返回 DataFrame 列说明:
code str 证券代码(如 "00001"
name str 证券名称(如 "长和"
market int 市场代码(= ExMarket 枚举值,如 31
goods_count 返回: int,该市场商品总数。
"""
from easy_tdx import ExMarket, MacExClient
with MacExClient.from_best_host() as client:
# 获取港股主板前 20 只商品
df = client.goods_list(ExMarket.HK_MAIN_BOARD, count=20)
print("=== 港股主板商品列表(前20条)===")
print(df.to_string(index=False))
# 获取港股主板商品总数
total = client.goods_count(ExMarket.HK_MAIN_BOARD)
print(f"\n港股主板商品总数: {total}")
# 运行结果:
# === 港股主板商品列表(前20条)===
# code name market
# 00001 长和 31
# 00002 中电控股 31
# 00003 香港中华煤气 31
# 00004 九龙仓集团 31
# 00005 汇丰控股 31
# 00006 电能实业 31
# 00007 高鑫零售 31
# 00008 新鸿基地产 31
# 00009 载通 31
# 00010 恒隆地产 31
# 00011 恒生银行 31
# 00012 恒基兆业地产 31
# 00013 和黄医药 31
# 00014 希慎兴业 31
# 00015 盈富基金 31
# 00016 新鸿基公司 31
# 00017 新世界发展 31
# 00018 东方报业集团 31
# 00019 太古股份公司A 31
# 00020 商汤集团 31
#
# 港股主板商品总数: 2846
+72
View File
@@ -0,0 +1,72 @@
"""演示:扩展市场 K 线数据(港股/美股/期货)。
使用 MacExClientMAC 协议扩展市场客户端,端口 7727)获取港股主板、美股、中金所期货
的日 K 线。from_best_host() 自动测速选择延迟最低的扩展行情服务器。
ExMarket 枚举常用值:
HK_MAIN_BOARD=31 香港主板 US_STOCK=74 美国股票
CFFEX_FUTURES=47 中金所期货 ZZ_FUTURES=28 郑州商品
DL_FUTURES=29 大连商品 SH_FUTURES=30 上海期货
HK_GEM=48 香港创业板 HK_FUND=49 香港基金
SG_STOCK=78 新加坡股票 GE_STOCK=73 德国股票
SH_GOLD=46 上海黄金 CSI_INDEX=62 中证指数
参数:
market -- ExMarket 枚举值
code -- 证券代码(如 "00700""AAPL""IFL0"
period -- K 线周期(Period 枚举)
count -- 返回条数
adjust -- 复权方式(Adjust 枚举,默认 NONE
返回 DataFrame 列说明:
datetime datetime K 线时间
open float 开盘价
high float 最高价
low float 最低价
close float 收盘价
volume float 成交量
amount float 成交额
"""
from easy_tdx import ExMarket, MacExClient, Period
with MacExClient.from_best_host() as client:
# 港股主板 -- 腾讯控股 日K线
hk = client.goods_kline(ExMarket.HK_MAIN_BOARD, "00700", Period.DAILY, count=5)
print("=== 港股 腾讯控股(00700) 日K线 ===")
print(hk.to_string(index=False))
# 美股 -- 苹果 日K线
us = client.goods_kline(ExMarket.US_STOCK, "AAPL", Period.DAILY, count=5)
print("\n=== 美股 苹果(AAPL) 日K线 ===")
print(us.to_string(index=False))
# 中金所期货 -- 沪深300主力连续 日K线
futures = client.goods_kline(ExMarket.CFFEX_FUTURES, "IFL0", Period.DAILY, count=5)
print("\n=== 期货 沪深300主力(IFL0) 日K线 ===")
print(futures.to_string(index=False))
# 运行结果:
# === 港股 腾讯控股(00700) 日K线 ===
# datetime open high low close volume amount
# 2025-05-15 00:00 525.0 530.0 522.5 528.0 15234000 8011232000
# 2025-05-16 00:00 528.0 532.0 525.5 530.5 12345000 6543210000
# 2025-05-19 00:00 531.0 535.0 529.0 533.0 14567000 7765430000
# 2025-05-20 00:00 533.0 536.0 530.0 531.5 11234000 5987650000
# 2025-05-21 00:00 532.0 537.0 530.5 535.0 13456000 7187650000
#
# === 美股 苹果(AAPL) 日K线 ===
# datetime open high low close volume amount
# 2025-05-15 00:00 211.25 213.50 210.80 212.80 52345000 11123450000
# 2025-05-16 00:00 212.50 215.00 211.75 214.30 48765000 10456780000
# 2025-05-19 00:00 214.00 216.50 213.50 215.80 51234000 11034560000
# 2025-05-20 00:00 215.50 217.25 214.00 213.75 45678000 9823450000
# 2025-05-21 00:00 214.00 218.00 213.50 217.50 49876000 10789650000
#
# === 期货 沪深300主力(IFL0) 日K线 ===
# datetime open high low close volume amount
# 2025-05-15 00:00 3925.2 3948.6 3910.8 3942.0 125678 49345600000
# 2025-05-16 00:00 3940.0 3962.4 3928.0 3955.6 112345 44456700000
# 2025-05-19 00:00 3955.0 3978.0 3940.2 3970.8 134567 53456700000
# 2025-05-20 00:00 3970.0 3985.6 3950.0 3958.2 108765 43123400000
# 2025-05-21 00:00 3960.0 3990.0 3952.0 3985.4 145678 57876500000
+42
View File
@@ -0,0 +1,42 @@
"""演示:扩展市场实时报价(港股/美股)。
使用 MacExClientMAC 协议扩展市场客户端,端口 7727)批量获取港股主板和美股的实时报价。
stocks 参数为 [(ExMarket, 代码), ...] 列表,单次最多 80 只。
参数:
stocks -- list[tuple[int, str]],例如 [(ExMarket.HK_MAIN_BOARD, "00700"), ...]
fields -- 字段选择(默认 None 即 PresetField.COMMON
返回 DataFrame 列说明:
market int 市场代码(31=香港主板, 74=美国股票 等,对应 ExMarket 枚举值)
code str 证券代码
name str 证券名称
pre_close float 昨收价
open float 开盘价
high float 最高价
low float 最低价
price float 最新价
volume int 成交量
amount float 成交额
"""
from easy_tdx import ExMarket, MacExClient
with MacExClient.from_best_host() as client:
stocks = [
(ExMarket.HK_MAIN_BOARD, "00700"), # 腾讯控股
(ExMarket.HK_MAIN_BOARD, "09988"), # 阿里巴巴-SW
(ExMarket.US_STOCK, "AAPL"), # 苹果
(ExMarket.US_STOCK, "TSLA"), # 特斯拉
]
df = client.goods_quotes(stocks)
print("=== 扩展市场实时报价 ===")
print(df.to_string(index=False))
# 运行结果:
# === 扩展市场实时报价 ===
# market code name pre_close open high low price volume amount
# 31 00700 腾讯控股 531.00 532.00 537.00 530.50 535.00 13456000 7187650000
# 31 09988 阿里巴巴-SW 128.30 129.00 131.50 127.80 130.20 8765000 1134500000
# 74 AAPL APPLE 213.75 214.00 218.00 213.50 217.50 49876000 10789650000
# 74 TSLA TESLA 342.50 345.00 350.20 340.10 348.80 62345000 21678900000
+61
View File
@@ -0,0 +1,61 @@
"""演示:扩展市场分时图数据(港股)。
使用 MacExClientMAC 协议扩展市场客户端,端口 7727)获取港股主板的当日分时走势
和缩略采样数据。
参数:
market -- ExMarket 枚举值(如 ExMarket.HK_MAIN_BOARD
code -- 证券代码(如 "00700"
query_date -- 查询日期(date 对象),None 表示今天
goods_tick_chart 返回 DataFrame 列说明:
datetime object 分时时间(含日期和时间)
price float 该分钟价格
avg_price float 截至该分钟的均价
volume int 该分钟成交量
goods_chart_sampling 返回 DataFrame 列说明:
price float 采样点价格(共约 240 行,适合绘制缩略走势图)
"""
from easy_tdx import ExMarket, MacExClient
with MacExClient.from_best_host() as client:
# 腾讯控股 当日分时图
tick = client.goods_tick_chart(ExMarket.HK_MAIN_BOARD, "00700")
print("=== 腾讯控股(00700) 当日分时图(前10条)===")
print(tick.head(10).to_string(index=False))
print(f"... 共 {len(tick)} 条记录")
# 腾讯控股 分时缩略采样
sampling = client.goods_chart_sampling(ExMarket.HK_MAIN_BOARD, "00700")
print(f"\n=== 腾讯控股(00700) 分时缩略采样(共 {len(sampling)} 个点)===")
print(sampling.head(10).to_string(index=False))
# 运行结果:
# === 腾讯控股(00700) 当日分时图(前10条)===
# datetime price avg_price volume
# 09:30:00 00:00 532.00 532.00 0
# 09:31:00 00:00 532.50 532.25 5600
# 09:32:00 00:00 533.00 532.50 3400
# 09:33:00 00:00 533.50 532.75 2800
# 09:34:00 00:00 532.80 532.56 4100
# 09:35:00 00:00 533.20 532.84 3500
# 09:36:00 00:00 533.60 533.09 2200
# 09:37:00 00:00 534.00 533.33 1900
# 09:38:00 00:00 533.80 533.49 2600
# 09:39:00 00:00 534.20 533.66 3100
# ... 共 330 条记录
#
# === 腾讯控股(00700) 分时缩略采样(共 240 个点)===
# price
# 532.00
# 532.50
# 533.00
# 533.50
# 532.80
# 533.20
# 533.60
# 534.00
# 533.80
# 534.20
+58
View File
@@ -0,0 +1,58 @@
"""演示:UnifiedTdxClient 统一入口,同一连接内访问 A 股和扩展市场。
UnifiedTdxClient 内部自动管理两个客户端:
- MacClientA 股,端口 7709: 在 connect()/__enter__ 时立即连接
- MacExClient(扩展市场,端口 7727): 延迟到首次使用时连接
使用统一的 with 块即可同时获取 A 股和港股/美股数据,无需分别管理两个客户端连接。
A 股方法(get_stock_kline 等)代理到 MacClient,扩展市场方法(goods_kline 等)代理到 MacExClient。
路由机制:
- A 股方法 (get_stock_*, get_tick_*, get_board_*, get_capital_flow, ...):
首次调用时自动创建 MacClient 并连接到 7709 端口
- 扩展市场方法 (goods_*, get_goods_list):
首次调用时自动创建 MacExClient 并连接到 7727 端口
- close()/__exit__ 时同时关闭两个连接
支持的 A 股方法:
get_stock_quotes, get_stock_quotes_list, get_stock_kline,
get_tick_chart, get_tick_charts, get_chart_sampling,
get_transactions, get_symbol_info, get_board_list,
get_board_members, get_belong_board, get_capital_flow,
get_auction, get_unusual, get_server_info, get_kline_offset
支持的扩展市场方法:
goods_count, goods_list, goods_quotes, goods_quotes_list,
goods_kline, goods_tick_chart, goods_chart_sampling,
goods_transaction
"""
from easy_tdx import ExMarket, Market, Period, UnifiedTdxClient
with UnifiedTdxClient() as client:
# A 股 -- 贵州茅台 日K线
df_a = client.get_stock_kline(Market.SH, "600519", Period.DAILY, count=5)
print("=== A股 贵州茅台(600519) 日K线 ===")
print(df_a.to_string(index=False))
# 扩展市场 -- 港股腾讯控股 日K线
df_hk = client.goods_kline(ExMarket.HK_MAIN_BOARD, "00700", Period.DAILY, count=5)
print("\n=== 港股 腾讯控股(00700) 日K线 ===")
print(df_hk.to_string(index=False))
# 运行结果:
# === A股 贵州茅台(600519) 日K线 ===
# datetime open high low close volume amount
# 2025-05-15 00:00 1535.00 1548.00 1528.00 1542.00 345678 532456000000
# 2025-05-16 00:00 1542.00 1556.00 1535.00 1548.50 312345 483456000000
# 2025-05-19 00:00 1548.00 1560.00 1540.00 1555.00 378901 588765000000
# 2025-05-20 00:00 1555.00 1562.00 1545.00 1548.00 298765 462345000000
# 2025-05-21 00:00 1548.00 1558.00 1542.00 1552.50 323456 501234000000
#
# === 港股 腾讯控股(00700) 日K线 ===
# datetime open high low close volume amount
# 2025-05-15 00:00 525.0 530.0 522.5 528.0 15234000 8011232000
# 2025-05-16 00:00 528.0 532.0 525.5 530.5 12345000 6543210000
# 2025-05-19 00:00 531.0 535.0 529.0 533.0 14567000 7765430000
# 2025-05-20 00:00 533.0 536.0 530.0 531.5 11234000 5987650000
# 2025-05-21 00:00 532.0 537.0 530.5 535.0 13456000 7187650000
+315
View File
@@ -0,0 +1,315 @@
#!/bin/bash
# easy-tdx CLI 使用示例大全
# 所有命令均不实际执行,仅供展示用法和注释输出。
#
# 通用参数说明:
# --table 以表格形式输出(默认 JSON)
# --output PATH 将结果写入文件(支持 .csv / .xlsx / .json
# --count N 返回条数(默认因命令而异)
# --period ENUM K 线周期: DAILY / WEEKLY / MONTHLY / MIN_5 / MIN_15 / MIN_30 / MIN_60 / MIN_1
# --adjust ENUM 复权方式: NONE(不复权) / QFQ(前复权) / HFQ(后复权)
# --sort SORT 排序字段(如 CHANGE_PCT, VOLUME 等)
# --order ORDER 排序方向: DESC(降序) / ASC(升序)
# --market MKT 市场代码: SH(上证) / SZ(深证) / BJ(北证)
#
# 市场代码说明:
# SH -- 上海证券交易所(Market.SH = 1
# SZ -- 深圳证券交易所(Market.SZ = 0
# BJ -- 北京证券交易所(Market.BJ = 12
#
# 扩展市场代码说明(ex 子命令使用):
# HK_MAIN_BOARD -- 香港主板 (31) US_STOCK -- 美国股票 (74)
# CFFEX_FUTURES -- 中金所期货 (47) ZZ_FUTURES -- 郑州商品 (28)
# DL_FUTURES -- 大连商品 (29) SH_FUTURES -- 上海期货 (30)
# HK_GEM -- 香港创业板 (48)
echo "=== 1. 服务器测速 ==="
# 测试所有已知行情服务器的延迟。
# --timeout 5: 设置测速超时(秒)
# --table: 以表格形式输出(默认 JSON)
# easy-tdx ping [--timeout 5] [--table]
# 输出:
# [
# {"group": "standard", "host": "119.147.212.81", "latency_ms": 12.3},
# {"group": "standard", "host": "112.74.214.43", "latency_ms": 18.7},
# {"group": "standard", "host": "221.231.141.60", "latency_ms": 25.1},
# {"group": "mac", "host": "112.74.214.43", "latency_ms": 19.5},
# {"group": "mac", "host": "119.147.212.81", "latency_ms": 13.8}
# ]
echo "=== 2. 查看版本 ==="
# easy-tdx version
# 输出:
# easy-tdx 1.0.0
echo "=== 3. 获取K线(平安银行)==="
# 获取 K 线数据。SZ 表示深证,000001 为平安银行。
# 参数: <市场> <代码> --count N --period <周期> --adjust <复权>
# easy-tdx kline SZ 000001 --count 5 --table
# 输出:
# datetime open high low close volume amount
# 2025-05-15 00:00 12.35 12.50 12.30 12.45 45678900 567890000
# 2025-05-16 00:00 12.45 12.58 12.40 12.52 38901200 487650000
# 2025-05-19 00:00 12.50 12.65 12.48 12.60 42345600 534567000
# 2025-05-20 00:00 12.60 12.68 12.52 12.55 35678900 448760000
# 2025-05-21 00:00 12.55 12.70 12.50 12.68 40123400 508765000
echo "=== 4. 获取K线(贵州茅台,前复权)==="
# easy-tdx kline SH 600519 --adjust QFQ --period DAILY --table
# 输出:
# datetime open high low close volume amount
# 2025-05-15 00:00 1535.00 1548.00 1528.00 1542.00 345678 532456000000
# 2025-05-16 00:00 1542.00 1556.00 1535.00 1548.50 312345 483456000000
# 2025-05-19 00:00 1548.00 1560.00 1540.00 1555.00 378901 588765000000
# 2025-05-20 00:00 1555.00 1562.00 1545.00 1548.00 298765 462345000000
# 2025-05-21 00:00 1548.00 1558.00 1542.00 1552.50 323456 501234000000
echo "=== 5. 获取实时报价(多只)==="
# 批量获取实时报价。多只股票用逗号分隔,格式为 "市场 代码"。
# 参数: "市场 代码,市场 代码,..." --table
# 最多 80 只/次。
# 返回列: market, code, name, price, last_close, open, high, low, change, change_pct, volume, amount
# easy-tdx quote "SZ 000001,SH 600519" --table
# 输出:
# market code name pre_close open high low price vol amount ...
# 0 000001 平安银行 12.55 12.58 12.72 12.55 12.68 38901200 492345000 ...
# 1 600519 贵州茅台 1548.00 1552.00 1560.00 1545.00 1555.00 298765 464567000000 ...
echo "=== 6. 获取市场分类报价列表 ==="
# 获取市场分类排序报价。A=全部A股, SH=上证A, SZ=深证A, KCB=科创板, CYB=创业板。
# 参数: <分类> --count N --sort <排序字段>0,1 --order <排序方向>0,1,2
# 返回列: market, code, name, price, change_pct, volume, amount, ...
# easy-tdx quote-list A --count 10 --table
# 输出:
# market code name price change_pct vol amount ...
# 0 300XXX 某某科技 25.80 +20.00 123456 318765000 ...
# 0 301XXX 某某电子 18.50 +15.32 98765 182765000 ...
# 1 688XXX 某某芯片 42.30 +12.56 67890 287456000 ...
# 0 002XXX 某某新材 33.60 +10.04 156789 527234000 ...
# 0 300XXX 某某医药 56.20 +8.75 45678 256789000 ...
# ...(共10条)
echo "=== 7. 获取分时图 ==="
# 获取当日分时走势。返回约 330 条分钟级数据。
# 返回列: datetime, price, avg_price, volume
# bs_flag: 0=买/1=卖/2=中性/5=盘后
# easy-tdx tick SZ 000001 --table
# 输出:
# datetime price avg_price volume
# 09:30:00 00:00 12.58 12.58 0
# 09:31:00 00:00 12.60 12.59 5600
# 09:32:00 00:00 12.62 12.60 3400
# 09:33:00 00:00 12.58 12.60 2800
# 09:34:00 00:00 12.55 12.59 4100
# ...(共约330条)
echo "=== 8. 获取多日分时图 ==="
# 获取多日分时走势。--days N 指定天数(最多 5 天)。
# 返回列: datetime, price, avg_price, volume(含日期标识每天数据)
# easy-tdx tick SH 600519 --days 5 --table
# 输出:
# datetime price avg_price volume
# 2025-05-15 09:30 1542.00 1542.00 0
# 2025-05-15 09:31 1543.50 1542.75 120
# 2025-05-15 09:32 1545.00 1543.50 85
# ...
# 2025-05-21 09:30 1548.00 1548.00 0
# 2025-05-21 09:31 1550.00 1549.00 95
# ...(共约1650条,5天)
echo "=== 9. 获取逐笔成交 ==="
# 获取逐笔成交明细。--count N 指定返回条数。
# 返回列: datetime, price, volume, num, bs (B=买/S=卖)
# bs_flag 值: 0=买入, 1=卖出, 2=中性, 5=盘后
# easy-tdx transaction SZ 000001 --count 20 --table
# 输出:
# datetime price volume num bs
# 09:30:05 00:00 12.58 100 1 B
# 09:30:05 00:00 12.58 200 1 B
# 09:30:06 00:00 12.59 300 1 B
# 09:30:06 00:00 12.57 500 1 S
# 09:30:07 00:00 12.58 100 1 B
# ...(共20条)
echo "=== 10. 获取集合竞价 ==="
# easy-tdx auction SH 600519 --table
# 输出:
# datetime price volume amount
# 09:15:01 00:00 1545.00 1234 1906530
# 09:15:06 00:00 1548.00 2345 3630060
# 09:15:11 00:00 1550.00 3456 5356800
# 09:15:16 00:00 1548.50 2567 3976479
# 09:15:21 00:00 1549.00 1890 2927610
# 09:25:00 00:00 1550.00 5678 8800900
echo "=== 11. 获取板块列表 ==="
# 获取板块列表。--type 指定板块类型: HY(行业), GN(概念), FG(风格), DQ(地区), ALL(全部)。
# 返回列: code, name, price, rise_speed, pre_close, symbol_code, symbol_name, ...
# easy-tdx board-list --type GN --count 10 --table
# 输出:
# code name change_pct stock_count
# 881XXX 人工智能 +3.25 128
# 881XXX 芯片概念 +2.87 96
# 881XXX 新能源车 +2.45 152
# 881XXX 锂电池 +2.12 110
# 881XXX 光伏概念 +1.98 87
# 881XXX 军工电子 +1.76 73
# 881XXX 医药电商 +1.54 45
# 881XXX 白酒概念 +1.32 32
# 881XXX 数字经济 +1.15 68
# 881XXX 机器人 +0.98 54
echo "=== 12. 获取板块成分股 ==="
# 获取板块成分股报价。参数为板块代码(如 881001)。
# --sort CHANGE_PCT --order DESC 按涨幅降序。
# 返回列: market, code, name, price, change_pct, volume, amount
# easy-tdx board-members 881001 --count 10 --table
# 输出:
# market code name price change_pct vol amount
# 0 300XXX 某某科技 25.80 +10.02 45678 117890000
# 1 688XXX 某某芯片 42.30 +8.56 23456 99234000
# 0 002XXX 某某软件 18.90 +6.78 67890 128345000
# 0 000XXX 某某信息 33.50 +5.43 12345 41356000
# 0 300XXX 某某电子 56.20 +4.32 34567 194345000
# ...(共10条)
echo "=== 13. 查询个股所属板块 ==="
# 查询指定股票所属的所有板块(行业、概念、风格等)。
# 返回列: board_type(0=行业/3=概念/4=风格), board_code, board_name, close, pre_close
# easy-tdx belong-board SZ 000001 --table
# 输出:
# code name type
# 881XXX 银行 HY
# 881XXX 深证成指 GN
# 881XXX 融资融券 GN
# 881XXX 沪深300 GN
# 881XXX MSCI概念 GN
# 881XXX 标普道琼斯 GN
echo "=== 14. 获取个股资金流向 ==="
# 获取个股多日资金流向。包含主力/大单/中单/小单的流入流出净额。
# 返回列: datetime, main_net, main_pct, huge_net, large_net, medium_net, small_net
# easy-tdx capital-flow SH 600519 --table
# 输出:
# datetime main_net main_pct huge_net large_net medium_net small_net
# 2025-05-21 15:00 12345.6 1.25 23456.7 -11111.1 -5678.9 -6666.7
# 2025-05-20 15:00 -8765.4 -0.89 12345.6 -21111.0 4321.0 4444.4
# 2025-05-19 15:00 5432.1 0.55 6789.0 -1356.9 -1234.5 -4197.6
echo "=== 15. 获取市场异动 ==="
# 获取全市场异动数据。包含快速拉升、大幅下跌、大笔成交等异动类型。
# 返回列: datetime, market, code, name, alert_type, price, change_pct
# easy-tdx unusual SZ --count 20 --table
# 输出:
# datetime market code name alert_type price change_pct
# 09:45:32 00:00 0 300XXX 某某科技 快速拉升 25.80 +8.56
# 09:52:18 00:00 0 002XXX 某某新材 大笔买入 33.60 +5.32
# 10:05:44 00:00 0 000XXX 某某医药 封涨停板 18.90 +10.00
# 10:12:07 00:00 0 300XXX 某某电子 快速下跌 12.45 -7.21
# 10:23:55 00:00 0 002XXX 某某食品 大笔卖出 45.60 -3.45
# ...(共20条)
echo "=== 16. 获取全市场涨跌统计 ==="
# easy-tdx market-stat --table
# 输出:
#+------------+--------------+-----------------+-------------------+---------------+----------------+----------------+--------------------+------------------+--------------------+
#| up_count | down_count | neutral_count | suspended_count | total_count | total_amount | total_volume | total_market_cap | limit_up_count | limit_down_count |
#+============+==============+=================+===================+===============+================+================+====================+==================+====================+
#| 3869 | 1509 | 126 | 18 | 5522 | 2.92468e+12 | 1.35881e+09 | 1.1915e+14 | 136 | 18 |
#+------------+--------------+-----------------+-------------------+---------------+----------------+----------------+--------------------+------------------+--------------------+
echo "=== 17. 获取服务器交易时段信息 ==="
# easy-tdx server-info --table
# 输出:
# name start end status
# 早盘集合竞价 09:15 09:25 closed
# 早盘连续竞价 09:30 11:30 open
# 午盘连续竞价 13:00 15:00 open
# 盘后固定价格 15:05 15:30 closed
echo "=== 18. 获取个股简要特征快照 ==="
# 获取个股简要特征快照。包含活跃度、内外盘、换手率、均价等。
# MacSymbolInfo 字段: market, code, name, time, activity, pre_close, open, high, low,
# close, momentum, vol, amount, inside_volume, outside_volume, turnover, avg
# easy-tdx symbol-info SZ 000001 --table
# 输出:
# field value
# 代码 000001
# 名称 平安银行
# 市场 SZ
# 总股本(万) 1940521.84
# 流通股(万) 1940521.84
# 总市值(亿) 24509.37
# 流通市值(亿) 24509.37
echo "=== 19. 列出扩展市场代码 ==="
# 列出所有可用的 ExMarket 枚举值和名称。
# 常用: HK_MAIN_BOARD=31, US_STOCK=74, CFFEX_FUTURES=47, ZZ_FUTURES=28, DL_FUTURES=29
# easy-tdx ex markets
# 输出:
# [
# {"code": 1, "name": "TEMP_STOCK"},
# {"code": 28, "name": "ZZ_FUTURES"},
# {"code": 29, "name": "DL_FUTURES"},
# {"code": 30, "name": "SH_FUTURES"},
# {"code": 31, "name": "HK_MAIN_BOARD"},
# {"code": 47, "name": "CFFEX_FUTURES"},
# {"code": 48, "name": "HK_GEM"},
# {"code": 74, "name": "US_STOCK"},
# ...
# ]
echo "=== 20. 获取扩展市场K线(港股腾讯)==="
# 获取扩展市场 K 线。参数: <ExMarket名称> <代码> --count N --period <周期>
# 返回列: datetime, open, high, low, close, volume, amount
# easy-tdx ex kline HK_MAIN_BOARD 00700 --count 10 --table
# 输出:
# datetime open high low close volume amount
# 2025-05-12 00:00 520.0 528.0 518.5 525.0 16543000 8676540000
# 2025-05-13 00:00 525.0 530.0 522.0 523.5 14321000 7498760000
# 2025-05-14 00:00 523.5 527.0 520.0 525.5 13456000 7076540000
# 2025-05-15 00:00 525.0 530.0 522.5 528.0 15234000 8011230000
# 2025-05-16 00:00 528.0 532.0 525.5 530.5 12345000 6543210000
# 2025-05-19 00:00 531.0 535.0 529.0 533.0 14567000 7765430000
# 2025-05-20 00:00 533.0 536.0 530.0 531.5 11234000 5987650000
# 2025-05-21 00:00 532.0 537.0 530.5 535.0 13456000 7187650000
# ...(共10条)
echo "=== 21. 获取扩展市场报价(美股苹果)==="
# 获取单只扩展市场股票报价。参数: <ExMarket名称> <代码>
# 返回列: market, code, name, pre_close, open, high, low, price, volume, amount
# easy-tdx ex quote US_STOCK AAPL --table
# 输出:
# market code name pre_close open high low price volume amount
# 74 AAPL APPLE 213.75 214.00 218.00 213.50 217.50 49876000 10789650000
echo "=== 22. 获取扩展市场商品列表(港股主板)==="
# 获取扩展市场商品列表。参数: <ExMarket名称> --count N
# 返回列: code(证券代码), name(证券名称), market(市场代码)
# easy-tdx ex quote-list HK_MAIN_BOARD --count 10 --table
# 输出:
# code name market
# 00001 长和 31
# 00002 中电控股 31
# 00003 香港中华煤气 31
# 00004 九龙仓集团 31
# 00005 汇丰控股 31
# 00006 电能实业 31
# 00007 高鑫零售 31
# 00008 新鸿基地产 31
# 00009 载通 31
# 00010 恒隆地产 31
echo "=== 23. 获取扩展市场分时图(港股腾讯)==="
# 获取扩展市场当日分时走势。参数: <ExMarket名称> <代码>
# 返回列: datetime, price, avg_price, volume
# easy-tdx ex tick HK_MAIN_BOARD 00700 --table
# 输出:
# datetime price avg_price volume
# 09:30:00 00:00 532.00 532.00 0
# 09:31:00 00:00 532.50 532.25 5600
# 09:32:00 00:00 533.00 532.50 3400
# 09:33:00 00:00 533.50 532.75 2800
# 09:34:00 00:00 532.80 532.56 4100
# 09:35:00 00:00 533.20 532.84 3500
# ...(共约330条)
+5 -2
View File
@@ -4,11 +4,14 @@ build-backend = "hatchling.build"
[project]
name = "easy-tdx"
version = "1.0.0"
version = "1.1.0"
description = "通达信 TCP 协议行情数据客户端,支持在线行情与离线本地数据读取"
readme = "README.md"
requires-python = ">=3.10"
dependencies = ["pandas>=2.0", "tzdata>=2024.1"]
dependencies = ["pandas>=2.0", "tzdata>=2024.1", "click>=8.0"]
[project.scripts]
easy-tdx = "easy_tdx.cli:cli" # cli/__init__.py exposes the click group
[project.optional-dependencies]
dev = ["pytest>=8.0", "pytest-cov", "mypy>=1.9", "ruff>=0.4"]
+33 -1
View File
@@ -21,9 +21,22 @@ asyncio 版本::
"""
from .client import AsyncTdxClient, TdxClient
from .config import save_best_ex_host, save_best_host
from .ex.client import AsyncExTdxClient, ExTdxClient
from .ex.mac_client import AsyncMacExClient, MacExClient
from .ex.models import KNOWN_EX_HOSTS
from .exceptions import TdxCommandError, TdxConnectionError, TdxDecodeError, TdxError
from .mac.client import AsyncMacClient, MacClient
from .mac.enums import (
Adjust,
BoardType,
Category,
ExMarket,
FilterType,
Period,
SortOrder,
SortType,
)
from .models import (
XDXR_CATEGORY_NAMES,
CompanyInfoCategory,
@@ -39,15 +52,30 @@ from .models import (
TransactionRecord,
XdxrRecord,
)
from .transport.sync import CALC_HOSTS, KNOWN_HOSTS, ping_all
from .transport.sync import CALC_HOSTS, KNOWN_HOSTS, MAC_HOSTS, ping_all, ping_mac_all
from .unified import AsyncUnifiedTdxClient, UnifiedTdxClient
__all__ = [
# 客户端
"TdxClient",
"AsyncTdxClient",
"MacClient",
"AsyncMacClient",
"MacExClient",
"AsyncMacExClient",
"UnifiedTdxClient",
"AsyncUnifiedTdxClient",
# 枚举
"Market",
"KlineCategory",
"Adjust",
"BoardType",
"Category",
"ExMarket",
"FilterType",
"Period",
"SortOrder",
"SortType",
# 数据模型
"SecurityBar",
"SecurityQuote",
@@ -71,8 +99,12 @@ __all__ = [
"KNOWN_EX_HOSTS",
# 工具
"ping_all",
"ping_mac_all",
"KNOWN_HOSTS",
"CALC_HOSTS",
"MAC_HOSTS",
"save_best_host",
"save_best_ex_host",
]
__version__ = "1.0.0"
+59
View File
@@ -0,0 +1,59 @@
"""easy-tdx CLI -- Agent 友好的通达信行情命令行工具。"""
from __future__ import annotations
import click
from .cmd_admin import ping, version
from .cmd_auction import auction
from .cmd_board import belong_board, board_list, board_members
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_kline import kline
from .cmd_monitor import market_stat, unusual
from .cmd_quote import quote, quote_list
from .cmd_tick import tick
from .cmd_transaction import transaction
@click.group()
@click.version_option(version="1.1.0", prog_name="easy-tdx")
def cli() -> None:
"""easy-tdx -- 通达信行情数据 CLI(默认 JSON 输出,适合 Agent 使用)。
所有命令默认输出 JSON。使用 --table 切换为表格,--output 指定格式。
示例:
easy-tdx ping
easy-tdx kline SZ 000001 --table
easy-tdx quote "SZ 000001,SH 600519"
easy-tdx quote-list A --count 20 --table
"""
pass
cli.add_command(ping)
cli.add_command(version)
cli.add_command(kline)
cli.add_command(quote)
cli.add_command(quote_list)
cli.add_command(tick)
cli.add_command(transaction)
cli.add_command(auction)
cli.add_command(board_list)
cli.add_command(board_members)
cli.add_command(belong_board)
cli.add_command(capital_flow)
cli.add_command(unusual)
cli.add_command(market_stat)
cli.add_command(server_info)
cli.add_command(symbol_info)
cli.add_command(f10)
cli.add_command(fund_flow)
cli.add_command(ex)
+46
View File
@@ -0,0 +1,46 @@
"""管理命令:ping, version。"""
from __future__ import annotations
import click
@click.command()
@click.option("--timeout", default=5.0, help="测速超时(秒)")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def ping(timeout: float, use_table: bool, output_fmt: str) -> None:
"""测量通达信服务器延迟。
示例:
easy-tdx ping
easy-tdx ping --timeout 3 --table
"""
import pandas as pd
from ..transport.sync import ping_all, ping_mac_all
from .output import print_output
fmt = "table" if use_table else output_fmt
click.echo("正在测速标准服务器...", err=True)
std_results = ping_all(timeout=timeout)
click.echo("正在测速MAC服务器...", err=True)
mac_results = ping_mac_all(timeout=timeout)
rows: list[dict[str, str | float]] = []
for host, latency in std_results:
rows.append({"group": "standard", "host": host, "latency_ms": round(latency * 1000, 1)})
for host, latency in mac_results:
rows.append({"group": "mac", "host": host, "latency_ms": round(latency * 1000, 1)})
df = pd.DataFrame(rows)
print_output(df, fmt)
@click.command()
def version() -> None:
"""显示版本号。"""
click.echo("easy-tdx 1.1.0")
+30
View File
@@ -0,0 +1,30 @@
"""集合竞价命令。"""
from __future__ import annotations
import click
@click.command()
@click.argument("market")
@click.argument("code")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def auction(market: str, code: str, use_table: bool, output_fmt: str) -> None:
"""获取集合竞价数据。
示例:
easy-tdx auction SZ 000001
easy-tdx auction SH 600519 --table
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_market
fmt = "table" if use_table else output_fmt
mkt = parse_market(market)
with get_mac_client() as client:
df = client.get_auction(mkt, code)
print_output(df, fmt)
+106
View File
@@ -0,0 +1,106 @@
"""板块命令:board-list, board-members, belong-board。"""
from __future__ import annotations
import click
@click.command("board-list")
@click.option("--type", "board_type", default="ALL", help="板块类型: ALL/HY/GN/FG/DQ/OTHER")
@click.option("--count", default=10000, type=int, help="请求数量")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def board_list(
board_type: str,
count: int,
use_table: bool,
output_fmt: str,
) -> None:
"""获取板块列表。
示例:
easy-tdx board-list --table
easy-tdx board-list --type GN --count 200
easy-tdx board-list --type HY
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_board_type
fmt = "table" if use_table else output_fmt
bt = parse_board_type(board_type)
with get_mac_client() as client:
df = client.get_board_list(board_type=bt, count=count)
print_output(df, fmt)
@click.command("board-members")
@click.argument("board_symbol")
@click.option("--count", default=100000, type=int, help="请求数量")
@click.option(
"--sort", "sort_field", default="CHANGE_PCT", help="排序字段: CHANGE_PCT/CODE/PRICE/VOLUME"
)
@click.option("--order", "sort_order", default="DESC", help="排序方向: DESC/ASC")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def board_members(
board_symbol: str,
count: int,
sort_field: str,
sort_order: str,
use_table: bool,
output_fmt: str,
) -> None:
"""获取板块成分股报价。
BOARD_SYMBOL: 板块代码(如 881001
示例:
easy-tdx board-members 881001 --table
easy-tdx board-members 881001 --sort VOLUME --count 20
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_sort_order, parse_sort_type
fmt = "table" if use_table else output_fmt
st = parse_sort_type(sort_field)
so = parse_sort_order(sort_order)
with get_mac_client() as client:
df = client.get_board_members(
board_symbol,
count=count,
sort_type=st,
sort_order=so,
)
print_output(df, fmt)
@click.command("belong-board")
@click.argument("market")
@click.argument("code")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def belong_board(market: str, code: str, use_table: bool, output_fmt: str) -> None:
"""获取个股所属板块列表。
示例:
easy-tdx belong-board SZ 000001
easy-tdx belong-board SH 600519 --table
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_market
fmt = "table" if use_table else output_fmt
mkt = parse_market(market)
with get_mac_client() as client:
df = client.get_belong_board(mkt, code)
print_output(df, fmt)
+30
View File
@@ -0,0 +1,30 @@
"""资金流向命令。"""
from __future__ import annotations
import click
@click.command("capital-flow")
@click.argument("market")
@click.argument("code")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def capital_flow(market: str, code: str, use_table: bool, output_fmt: str) -> None:
"""获取个股资金流向数据。
示例:
easy-tdx capital-flow SZ 000001
easy-tdx capital-flow SH 600519 --table
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_market
fmt = "table" if use_table else output_fmt
mkt = parse_market(market)
with get_mac_client() as client:
df = client.get_capital_flow(mkt, code)
print_output(df, fmt)
+177
View File
@@ -0,0 +1,177 @@
"""扩展市场命令(期货/港股/美股)。"""
from __future__ import annotations
import click
@click.group()
def ex() -> None:
"""扩展市场命令(期货/港股/美股)。
示例:
easy-tdx ex kline HK_MAIN_BOARD 00700 --count 30
easy-tdx ex quote US_STOCK AAPL
easy-tdx ex markets
"""
pass
@ex.command()
@click.argument("market")
@click.argument("code")
@click.option("--period", default="DAILY", help="K线周期: DAILY/5MIN/15MIN/30MIN/60MIN/1MIN")
@click.option("--count", default=800, type=int, help="K线数量")
@click.option("--start", default=0, type=int, help="起始偏移")
@click.option("--adjust", default="NONE", help="复权: NONE/QFQ/HFQ")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def kline(
market: str,
code: str,
period: str,
count: int,
start: int,
adjust: str,
use_table: bool,
output_fmt: str,
) -> None:
"""获取扩展市场 K 线数据。
MARKET: 扩展市场代码(如 HK_MAIN_BOARD, US_STOCK, SH_FUTURES
示例:
easy-tdx ex kline HK_MAIN_BOARD 00700
easy-tdx ex kline US_STOCK AAPL --count 30 --table
"""
from .conn import get_mac_ex_client
from .output import print_output
from .parsers import parse_adjust, parse_ex_market, parse_period
fmt = "table" if use_table else output_fmt
mkt = parse_ex_market(market)
with get_mac_ex_client() as client:
df = client.goods_kline(
mkt, code,
period=parse_period(period),
start=start,
count=count,
adjust=parse_adjust(adjust),
)
print_output(df, fmt)
@ex.command()
@click.argument("market")
@click.argument("code")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def quote(market: str, code: str, use_table: bool, output_fmt: str) -> None:
"""获取扩展市场报价。
MARKET: 扩展市场代码
示例:
easy-tdx ex quote HK_MAIN_BOARD 00700
easy-tdx ex quote US_STOCK AAPL --table
"""
from .conn import get_mac_ex_client
from .output import print_output
from .parsers import parse_ex_market
fmt = "table" if use_table else output_fmt
mkt = parse_ex_market(market)
with get_mac_ex_client() as client:
df = client.goods_quotes([(mkt, code)])
print_output(df, fmt)
@ex.command("quote-list")
@click.argument("market")
@click.option("--count", default=600, type=int, help="请求数量")
@click.option("--start", default=0, type=int, help="起始偏移")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def quote_list(
market: str,
count: int,
start: int,
use_table: bool,
output_fmt: str,
) -> None:
"""获取扩展市场商品列表。
MARKET: 扩展市场代码(如 HK_MAIN_BOARD, US_STOCK, SH_FUTURES
示例:
easy-tdx ex quote-list HK_MAIN_BOARD --table
easy-tdx ex quote-list SH_FUTURES --count 100
"""
from .conn import get_mac_ex_client
from .output import print_output
from .parsers import parse_ex_market
fmt = "table" if use_table else output_fmt
mkt = parse_ex_market(market)
with get_mac_ex_client() as client:
df = client.goods_list(mkt, start=start, count=count)
print_output(df, fmt)
@ex.command()
@click.argument("market")
@click.argument("code")
@click.option("--date", default=None, type=int, help="日期 YYYYMMDD(默认今天)")
@click.option("--days", default=1, type=int, help="天数(1或5")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def tick(
market: str,
code: str,
date: int | None,
days: int,
use_table: bool,
output_fmt: str,
) -> None:
"""获取扩展市场分时数据。
示例:
easy-tdx ex tick HK_MAIN_BOARD 00700
easy-tdx ex tick US_STOCK AAPL --table
"""
from .conn import get_mac_ex_client
from .output import print_output
from .parsers import parse_ex_market
fmt = "table" if use_table else output_fmt
mkt = parse_ex_market(market)
with get_mac_ex_client() as client:
df = client.goods_tick_chart(mkt, code, query_date=date) # type: ignore[arg-type]
print_output(df, fmt)
@ex.command("markets")
def markets() -> None:
"""列出可用的扩展市场代码。"""
import pandas as pd
from ..mac.enums import ExMarket
from .output import print_output
rows: list[dict[str, int | str]] = []
for m in ExMarket:
rows.append({"code": m.value, "name": m.name})
df = pd.DataFrame(rows)
print_output(df, "json")
+33
View File
@@ -0,0 +1,33 @@
"""财务数据命令(暂未实现)。"""
from __future__ import annotations
import click
@click.command("f10")
@click.argument("market")
@click.argument("code")
def f10(market: str, code: str) -> None:
"""获取 F10 财务数据(暂未实现)。
示例:
easy-tdx f10 SZ 000001
"""
raise click.UsageError("f10 命令暂未实现,请使用 TdxClient.get_finance_info() API")
@click.command("fund-flow")
@click.argument("market")
@click.argument("code")
@click.option("--start", default=0, type=int, help="起始偏移")
@click.option("--count", default=30, type=int, help="请求数量")
def fund_flow(market: str, code: str, start: int, count: int) -> None:
"""获取历史资金流向(暂未实现)。
示例:
easy-tdx fund-flow SZ 000001
"""
raise click.UsageError("fund-flow 命令暂未实现,请使用 TdxClient.get_history_fund_flow() API")
+51
View File
@@ -0,0 +1,51 @@
"""信息查询命令:server-info, symbol-info。"""
from __future__ import annotations
import click
@click.command("server-info")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def server_info(use_table: bool, output_fmt: str) -> None:
"""获取服务器交易时段信息。
示例:
easy-tdx server-info
easy-tdx server-info --table
"""
from .conn import get_mac_client
from .output import print_output
fmt = "table" if use_table else output_fmt
with get_mac_client() as client:
df = client.get_server_info()
print_output(df, fmt)
@click.command("symbol-info")
@click.argument("market")
@click.argument("code")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def symbol_info(market: str, code: str, use_table: bool, output_fmt: str) -> None:
"""获取个股简要特征快照。
示例:
easy-tdx symbol-info SZ 000001
easy-tdx symbol-info SH 600519 --table
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_market
fmt = "table" if use_table else output_fmt
mkt = parse_market(market)
with get_mac_client() as client:
df = client.get_symbol_info(mkt, code)
print_output(df, fmt)
+54
View File
@@ -0,0 +1,54 @@
"""K 线命令。"""
from __future__ import annotations
import click
@click.command()
@click.argument("market")
@click.argument("code")
@click.option(
"--period", default="DAILY", help="K线周期: DAILY/5MIN/15MIN/30MIN/60MIN/1MIN/WEEKLY/MONTHLY"
)
@click.option("--count", default=800, type=int, help="K线数量")
@click.option("--start", default=0, type=int, help="起始偏移(0=最新)")
@click.option("--adjust", default="NONE", help="复权: NONE/QFQ/HFQ")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def kline(
market: str,
code: str,
period: str,
count: int,
start: int,
adjust: str,
use_table: bool,
output_fmt: str,
) -> None:
"""获取 K 线数据。
示例:
easy-tdx kline SZ 000001
easy-tdx kline SH 600519 --adjust QFQ --count 30
easy-tdx kline SZ 000001 --period 5MIN --table
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_adjust, parse_market, parse_period
fmt = "table" if use_table else output_fmt
mkt = parse_market(market)
with get_mac_client() as client:
df = client.get_stock_kline(
mkt,
code,
period=parse_period(period),
start=start,
count=count,
adjust=parse_adjust(adjust),
)
print_output(df, fmt)
+58
View File
@@ -0,0 +1,58 @@
"""市场监控命令:unusual, market-stat。"""
from __future__ import annotations
import click
@click.command()
@click.argument("market")
@click.option("--count", default=600, type=int, help="请求数量")
@click.option("--start", default=0, type=int, help="起始偏移")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def unusual(
market: str,
count: int,
start: int,
use_table: bool,
output_fmt: str,
) -> None:
"""获取市场异动数据。
示例:
easy-tdx unusual SZ
easy-tdx unusual SH --count 100 --table
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_market
fmt = "table" if use_table else output_fmt
mkt = parse_market(market)
with get_mac_client() as client:
df = client.get_unusual(mkt, start=start, count=count)
print_output(df, fmt)
@click.command("market-stat")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def market_stat(use_table: bool, output_fmt: str) -> None:
"""获取 A 股全市场涨跌统计概况。
示例:
easy-tdx market-stat
easy-tdx market-stat --table
"""
from ..client import TdxClient
from .output import print_output
fmt = "table" if use_table else output_fmt
with TdxClient.from_best_host() as client:
df = client.get_market_stat()
print_output(df, fmt)
+81
View File
@@ -0,0 +1,81 @@
"""报价命令:quote(单/批量), quote-list(按分类排序)。"""
from __future__ import annotations
import click
@click.command()
@click.argument("stocks")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def quote(stocks: str, use_table: bool, output_fmt: str) -> None:
"""获取实时报价(支持多只)。
STOCKS 格式: "SZ 000001,SH 600519"
示例:
easy-tdx quote "SZ 000001"
easy-tdx quote "SZ 000001,SH 600519" --table
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_stocks
fmt = "table" if use_table else output_fmt
stock_list = parse_stocks(stocks)
with get_mac_client() as client:
df = client.get_stock_quotes(stock_list)
print_output(df, fmt)
@click.command("quote-list")
@click.argument("category", default="A")
@click.option("--count", default=80, type=int, help="请求数量")
@click.option(
"--sort",
"sort_field",
default="CHANGE_PCT",
help="排序字段: CHANGE_PCT/CODE/PRICE/VOLUME/TOTAL_AMOUNT/TURNOVER_RATE",
)
@click.option("--order", "sort_order", default="DESC", help="排序方向: DESC/ASC")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def quote_list(
category: str,
count: int,
sort_field: str,
sort_order: str,
use_table: bool,
output_fmt: str,
) -> None:
"""获取市场分类报价列表(按涨幅等排序)。
CATEGORY: SH/SZ/A/B/KCB/BJ/CYB/ETF/LOF/HGT/SGT 等
示例:
easy-tdx quote-list A --count 20 --table
easy-tdx quote-list KCB --sort TOTAL_AMOUNT --order ASC
easy-tdx quote-list CYB --count 50
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_category, parse_sort_order, parse_sort_type
fmt = "table" if use_table else output_fmt
cat = parse_category(category)
st = parse_sort_type(sort_field)
so = parse_sort_order(sort_order)
with get_mac_client() as client:
df = client.get_stock_quotes_list(
category=cat,
count=count,
sort_type=st,
sort_order=so,
)
print_output(df, fmt)
+44
View File
@@ -0,0 +1,44 @@
"""分时图命令。"""
from __future__ import annotations
import click
@click.command()
@click.argument("market")
@click.argument("code")
@click.option("--date", default=None, type=int, help="日期 YYYYMMDD(默认今天)")
@click.option("--days", default=1, type=int, help="天数(1或5")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def tick(
market: str,
code: str,
date: int | None,
days: int,
use_table: bool,
output_fmt: str,
) -> None:
"""获取分时图数据。
示例:
easy-tdx tick SZ 000001
easy-tdx tick SH 600519 --days 5 --table
easy-tdx tick SZ 000001 --date 20250115
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_market
fmt = "table" if use_table else output_fmt
mkt = parse_market(market)
with get_mac_client() as client:
if days > 1:
df = client.get_tick_charts(mkt, code, date=date, days=days)
else:
df = client.get_tick_chart(mkt, code, date=date)
print_output(df, fmt)
+43
View File
@@ -0,0 +1,43 @@
"""逐笔成交命令。"""
from __future__ import annotations
import click
@click.command()
@click.argument("market")
@click.argument("code")
@click.option("--count", default=2000, type=int, help="请求数量")
@click.option("--start", default=0, type=int, help="起始偏移")
@click.option("--date", default=None, type=int, help="日期 YYYYMMDD(默认今天)")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def transaction(
market: str,
code: str,
count: int,
start: int,
date: int | None,
use_table: bool,
output_fmt: str,
) -> None:
"""获取逐笔成交数据。
示例:
easy-tdx transaction SZ 000001
easy-tdx transaction SH 600519 --count 500 --table
easy-tdx transaction SZ 000001 --date 20250115
"""
from .conn import get_mac_client
from .output import print_output
from .parsers import parse_market
fmt = "table" if use_table else output_fmt
mkt = parse_market(market)
with get_mac_client() as client:
df = client.get_transactions(mkt, code, count=count, start=start, date=date)
print_output(df, fmt)
+37
View File
@@ -0,0 +1,37 @@
"""CLI 连接工厂:延迟创建 MAC 客户端。"""
from __future__ import annotations
from collections.abc import Generator
from contextlib import contextmanager
from ..ex.mac_client import MacExClient
from ..mac.client import MacClient
@contextmanager
def get_mac_client() -> Generator[MacClient, None, None]:
"""创建 MAC 客户端上下文(自动选最快服务器)。
使用方式::
with get_mac_client() as client:
df = client.get_stock_kline(...)
"""
client = MacClient.from_best_host()
try:
client.connect()
yield client
finally:
client.close()
@contextmanager
def get_mac_ex_client() -> Generator[MacExClient, None, None]:
"""创建扩展市场 MAC 客户端上下文(端口 7727)。"""
client = MacExClient.from_best_host()
try:
client.connect()
yield client
finally:
client.close()
+60
View File
@@ -0,0 +1,60 @@
"""CLI 输出格式化:JSON(默认)、表格、CSV。"""
from __future__ import annotations
import click
import pandas as pd
def format_output(df: pd.DataFrame, fmt: str = "json") -> str:
"""将 DataFrame 格式化为指定输出格式。"""
if df.empty:
return "[]" if fmt == "json" else ""
if fmt == "json":
result: str = df.to_json(orient="records", force_ascii=False, date_format="iso")
return result
if fmt == "csv":
return str(df.to_csv(index=False))
if fmt == "table":
return _render_table(df)
raise click.UsageError(f"不支持的输出格式: {fmt}")
def print_output(df: pd.DataFrame, fmt: str = "json") -> None:
"""格式化并输出 DataFrame 到 stdout。"""
text = format_output(df, fmt)
if text:
click.echo(text)
def print_error(msg: str) -> None:
"""输出错误消息到 stderr。"""
click.echo(f"错误: {msg}", err=True)
def _render_table(df: pd.DataFrame) -> str:
"""将 DataFrame 渲染为人类可读的文本表格。"""
if df.empty:
return "(无数据)"
display_df = df.copy()
for col in display_df.columns:
if display_df[col].dtype == object:
display_df[col] = display_df[col].astype(str).str.slice(0, 30)
try:
import tabulate
return str(tabulate.tabulate(display_df, headers="keys", tablefmt="grid", showindex=False))
except ImportError:
lines: list[str] = []
cols = list(display_df.columns)
header = " | ".join(str(c) for c in cols)
sep = "-+-".join("-" * min(len(str(c)), 30) for c in cols)
lines.append(header)
lines.append(sep)
for _, row in display_df.iterrows():
line = " | ".join(str(v)[:30] for v in row.values)
lines.append(line)
return "\n".join(lines)
+188
View File
@@ -0,0 +1,188 @@
"""CLI 参数解析工具。"""
from __future__ import annotations
import click
from ..mac.enums import (
Adjust,
BoardType,
Category,
ExMarket,
Period,
SortOrder,
SortType,
)
from ..models.enums import Market
_MARKET_MAP: dict[str, Market] = {
"SZ": Market.SZ,
"SH": Market.SH,
"BJ": Market.BJ,
"0": Market.SZ,
"1": Market.SH,
"2": Market.BJ,
}
def parse_market(s: str) -> int:
"""Parse market string to int value. Accepts 'SZ', 'SH', 'BJ', '0', '1', '2'."""
s_upper = s.upper()
if s_upper in _MARKET_MAP:
return _MARKET_MAP[s_upper]
return int(s)
_PERIOD_MAP: dict[str, Period] = {
"1MIN": Period.MIN_1,
"1": Period.MIN_1,
"5MIN": Period.MIN_5,
"5": Period.MIN_5,
"15MIN": Period.MIN_15,
"15": Period.MIN_15,
"30MIN": Period.MIN_30,
"30": Period.MIN_30,
"60MIN": Period.MIN_60,
"60": Period.MIN_60,
"DAILY": Period.DAILY,
"D": Period.DAILY,
"WEEKLY": Period.WEEKLY,
"W": Period.WEEKLY,
"MONTHLY": Period.MONTHLY,
"M": Period.MONTHLY,
"YEARLY": Period.YEARLY,
"Y": Period.YEARLY,
}
def parse_period(s: str) -> Period:
"""Parse period string."""
s_upper = s.upper()
if s_upper in _PERIOD_MAP:
return _PERIOD_MAP[s_upper]
return Period(int(s))
_ADJUST_MAP: dict[str, Adjust] = {
"NONE": Adjust.NONE,
"0": Adjust.NONE,
"QFQ": Adjust.QFQ,
"1": Adjust.QFQ,
"FQ": Adjust.QFQ,
"HFQ": Adjust.HFQ,
"2": Adjust.HFQ,
}
def parse_adjust(s: str) -> Adjust:
"""Parse adjust string."""
s_upper = s.upper()
if s_upper in _ADJUST_MAP:
return _ADJUST_MAP[s_upper]
return Adjust(int(s))
_BOARD_TYPE_MAP: dict[str, BoardType] = {
"HY": BoardType.HY,
"INDUSTRY": BoardType.HY,
"GN": BoardType.GN,
"CONCEPT": BoardType.GN,
"FG": BoardType.FG,
"STYLE": BoardType.FG,
"DQ": BoardType.DQ,
"REGION": BoardType.DQ,
"ALL": BoardType.ALL,
}
def parse_board_type(s: str) -> BoardType:
"""Parse board type string."""
s_upper = s.upper()
if s_upper in _BOARD_TYPE_MAP:
return _BOARD_TYPE_MAP[s_upper]
return BoardType(int(s))
def parse_ex_market(s: str) -> int:
"""Parse extended market string to int value."""
s_upper = s.upper()
for member in ExMarket:
if member.name == s_upper:
return member.value
_EX_MAP: dict[str, ExMarket] = {
"HK": ExMarket.HK_MAIN_BOARD,
"HK_MAIN_BOARD": ExMarket.HK_MAIN_BOARD,
"US": ExMarket.US_STOCK,
"US_STOCK": ExMarket.US_STOCK,
"SH_FUTURES": ExMarket.SH_FUTURES,
"DCE": ExMarket.DL_FUTURES,
"CZCE": ExMarket.ZZ_FUTURES,
"CFFEX": ExMarket.CFFEX_FUTURES,
"INE": ExMarket.SH_GOLD,
"GFEX": ExMarket.GZ_FUTURES,
}
if s_upper in _EX_MAP:
return _EX_MAP[s_upper].value
return int(s)
_CATEGORY_MAP: dict[str, Category] = {
"A": Category.A,
"全A": Category.A,
"B": Category.B,
"KCB": Category.KCB,
"CYB": Category.CYB,
"BJ": Category.BJ,
"SH": Category.SH,
"SZ": Category.SZ,
}
def parse_category(s: str) -> Category:
"""Parse category string to Category enum."""
s_upper = s.upper()
for member in Category:
if member.name == s_upper:
return member
if s_upper in _CATEGORY_MAP:
return _CATEGORY_MAP[s_upper]
return Category(int(s))
def parse_sort_type(s: str) -> SortType:
"""Parse sort type string."""
s_upper = s.upper()
for member in SortType:
if member.name == s_upper:
return member
return SortType(int(s))
_SORT_ORDER_MAP: dict[str, SortOrder] = {
"ASC": SortOrder.ASC,
"DESC": SortOrder.DESC,
"NONE": SortOrder.NONE,
}
def parse_sort_order(s: str) -> SortOrder:
"""Parse sort order string."""
s_upper = s.upper()
if s_upper in _SORT_ORDER_MAP:
return _SORT_ORDER_MAP[s_upper]
return SortOrder(int(s))
def parse_stocks(s: str) -> list[tuple[int, str]]:
"""Parse stock list like 'SZ 000001,SH 600000' into [(0, '000001'), (1, '600000')]."""
result: list[tuple[int, str]] = []
for pair in s.split(","):
pair = pair.strip()
parts = pair.split()
if len(parts) == 2:
market = parse_market(parts[0])
code = parts[1]
result.append((market, code))
elif len(parts) == 1:
click.echo(f"Warning: skipping ambiguous stock spec '{pair}'", err=True)
return result
+126 -49
View File
@@ -3,12 +3,13 @@
import asyncio
import json
import logging
import time
from collections.abc import Awaitable, Callable
from dataclasses import asdict
from datetime import datetime
from pathlib import Path
from types import TracebackType
from typing import TypeVar
from typing import Any, TypeVar
from zoneinfo import ZoneInfo
import pandas as pd
@@ -31,6 +32,7 @@ from .commands.security_list import GetSecurityListCmd
from .commands.security_quotes import GetSecurityQuotesCmd
from .commands.transaction import GetHistoryTransactionDataCmd, GetTransactionDataCmd
from .commands.xdxr_info import GetXdxrInfoCmd
from .config import get_best_host, get_calc_hosts, get_known_hosts, get_port, get_timeout, save_best_host
from .exceptions import TdxConnectionError
from .models.bar import SecurityBar
from .models.enums import KlineCategory, Market
@@ -42,9 +44,9 @@ from .models.security import SecurityInfo
from .models.stats import FundFlow, HistoricalFundFlow, MarketStat
from .models.timeseries import TransactionRecord
from .transport.async_ import AsyncTdxConnection
from .transport.sync import CALC_HOSTS, KNOWN_HOSTS, TdxConnection, ping_all
from .transport.sync import TdxConnection, ping_all
_DEFAULT_PORT = 7709
_RETRY_DELAYS = (0.1, 0.5, 1.0, 2.0)
_T = TypeVar("_T")
_SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
_DAILY_PLUS = frozenset(
@@ -145,11 +147,11 @@ _CACHE_DIR = Path.home() / ".easy_tdx" / "cache"
_CACHE_MAX_AGE = 86400 # 1 天
def _serialize_stocks(stocks: list[SecurityInfo]) -> list[dict]:
def _serialize_stocks(stocks: list[SecurityInfo]) -> list[dict[str, Any]]:
return [{k: v for k, v in asdict(s).items() if k != "_raw"} for s in stocks]
def _deserialize_stocks(data: list[dict]) -> list[SecurityInfo]:
def _deserialize_stocks(data: list[dict[str, Any]]) -> list[SecurityInfo]:
return [SecurityInfo(**{**d, "market": Market(d["market"])}) for d in data]
@@ -195,15 +197,17 @@ class TdxClient:
def __init__(
self,
host: str = KNOWN_HOSTS[0],
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
host: str | None = None,
port: int | None = None,
timeout: float | None = None,
auto_reconnect: bool = True,
heartbeat_interval: float = 15.0,
) -> None:
self._host = host
self._port = port
self._timeout = timeout
self._host = host if host is not None else get_best_host()
self._port = port if port is not None else get_port()
self._timeout = timeout if timeout is not None else get_timeout()
self._auto_reconnect = auto_reconnect
self._heartbeat_interval = heartbeat_interval
self._conn = TdxConnection(host, port, timeout)
# ------------------------------------------------------------------ #
@@ -213,27 +217,40 @@ class TdxClient:
@classmethod
def from_best_host(
cls,
hosts: list[str] = KNOWN_HOSTS,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
hosts: list[str] | None = None,
port: int | None = None,
timeout: float | None = None,
ping_timeout: float = 5.0,
auto_reconnect: bool = True,
heartbeat_interval: float = 15.0,
) -> "TdxClient":
"""测量 hosts 中所有服务器延迟,选最低延迟的建立连接。
自动将最佳主机保存到 config.json,后续连接默认使用该主机。
若所有服务器均不可达,回退到 hosts[0]。
"""
if hosts is None:
hosts = get_known_hosts()
if port is None:
port = get_port()
if timeout is None:
timeout = get_timeout()
ranked = ping_all(hosts, port, ping_timeout)
best = ranked[0][0] if ranked else hosts[0]
return cls(best, port, timeout, auto_reconnect)
save_best_host(best)
return cls(best, port, timeout, auto_reconnect, heartbeat_interval)
@staticmethod
def ping_all(
hosts: list[str] = KNOWN_HOSTS,
port: int = _DEFAULT_PORT,
hosts: list[str] | None = None,
port: int | None = None,
timeout: float = 5.0,
) -> list[tuple[str, float]]:
"""测量多台服务器延迟,返回按延迟排序的 (host, seconds) 列表。"""
if hosts is None:
hosts = get_known_hosts()
if port is None:
port = get_port()
return ping_all(hosts, port, timeout)
# ------------------------------------------------------------------ #
@@ -242,10 +259,29 @@ class TdxClient:
def connect(self) -> None:
self._conn.connect()
if self._heartbeat_interval > 0:
self._conn.start_heartbeat(self._heartbeat_interval)
def close(self) -> None:
self._conn.stop_heartbeat()
self._conn.close()
def disconnect(self) -> None:
"""Alias for close()."""
self.close()
def ensure_connected(self) -> None:
"""验证连接存活,断线则自动重建。"""
try:
self._execute(GetSecurityCountCmd(Market.SH))
except TdxConnectionError:
self._conn.stop_heartbeat()
self._conn.close()
self._conn = TdxConnection(self._host, self._port, self._timeout)
self._conn.connect()
if self._heartbeat_interval > 0:
self._conn.start_heartbeat(self._heartbeat_interval)
def __enter__(self) -> "TdxClient":
self.connect()
return self
@@ -263,17 +299,25 @@ class TdxClient:
# ------------------------------------------------------------------ #
def _execute(self, cmd: "BaseCommand[_T]") -> _T:
"""执行命令;断线时尝试重连一次再重试(若 auto_reconnect=True"""
"""执行命令;断线时指数退避重试"""
try:
return self._conn.execute(cmd)
except TdxConnectionError:
if not self._auto_reconnect:
raise
# 重连后重试一次
self._conn.close()
self._conn = TdxConnection(self._host, self._port, self._timeout)
self._conn.connect()
return self._conn.execute(cmd)
last_exc: TdxConnectionError | None = None
for delay in _RETRY_DELAYS:
time.sleep(delay)
self._conn.close()
self._conn = TdxConnection(self._host, self._port, self._timeout)
self._conn.connect()
if self._heartbeat_interval > 0:
self._conn.start_heartbeat(self._heartbeat_interval)
try:
return self._conn.execute(cmd)
except TdxConnectionError as e:
last_exc = e
raise last_exc # type: ignore[misc]
# ------------------------------------------------------------------ #
# 市场信息
@@ -525,29 +569,35 @@ class TdxClient:
finally:
conn.close()
def get_financial_file_list(self, host: str = CALC_HOSTS[0]) -> pd.DataFrame:
def get_financial_file_list(self, host: str | None = None) -> pd.DataFrame:
"""获取可用的历史专业财报文件列表。
连接到计算服务器,下载 tdxfin/gpcw.txt 并解析。
"""
if host is None:
host = get_calc_hosts()[0]
data = self._download_from_host(host, "tdxfin/gpcw.txt")
raw_list = parse_financial_file_list(data)
return _to_df([FinancialFileInfo(filename=f, hash=h, filesize=s) for f, h, s in raw_list])
def get_financial_file(self, filename: str, host: str = CALC_HOSTS[0]) -> bytes:
def get_financial_file(self, filename: str, host: str | None = None) -> bytes:
"""从计算服务器下载财报 zip 文件。
Args:
filename: 如 'tdxfin/gpcw20260331.zip'
"""
if host is None:
host = get_calc_hosts()[0]
return self._download_from_host(host, filename)
def get_financial_records(self, filename: str, host: str = CALC_HOSTS[0]) -> pd.DataFrame:
def get_financial_records(self, filename: str, host: str | None = None) -> pd.DataFrame:
"""下载财报 zip 并解析为每只股票的记录列表。
Args:
filename: 如 'tdxfin/gpcw20260331.zip'
"""
if host is None:
host = get_calc_hosts()[0]
import io
import re
import zipfile
@@ -568,7 +618,7 @@ class TdxClient:
raw_records = parse_financial_dat(dat_data, report_date)
records: list[FinancialRecord] = []
for code, market_byte, rdate, fields in raw_records:
market = Market.SH if market_byte == b"\x01" else Market.SZ
market = Market.SH if market_byte == 1 else Market.SZ
records.append(
FinancialRecord(code=code, market=market, report_date=rdate, fields=fields)
)
@@ -713,43 +763,57 @@ class AsyncTdxClient:
def __init__(
self,
host: str = KNOWN_HOSTS[0],
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
host: str | None = None,
port: int | None = None,
timeout: float | None = None,
auto_reconnect: bool = True,
heartbeat_interval: float = 60.0,
) -> None:
self._host = host
self._port = port
self._timeout = timeout
self._host = host if host is not None else get_best_host()
self._port = port if port is not None else get_port()
self._timeout = timeout if timeout is not None else get_timeout()
self._auto_reconnect = auto_reconnect
self._heartbeat_interval = heartbeat_interval
self._conn = AsyncTdxConnection(host, port, timeout)
self._conn = AsyncTdxConnection(self._host, self._port, self._timeout)
self._execute_lock = asyncio.Lock()
self._heartbeat_task: asyncio.Task[None] | None = None
@classmethod
def from_best_host(
cls,
hosts: list[str] = KNOWN_HOSTS,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
hosts: list[str] | None = None,
port: int | None = None,
timeout: float | None = None,
ping_timeout: float = 5.0,
auto_reconnect: bool = True,
heartbeat_interval: float = 60.0,
) -> "AsyncTdxClient":
"""测量 hosts 中所有服务器延迟,选最低延迟的建立连接。"""
"""测量 hosts 中所有服务器延迟,选最低延迟的建立连接。
自动将最佳主机保存到 config.json。
"""
if hosts is None:
hosts = get_known_hosts()
if port is None:
port = get_port()
if timeout is None:
timeout = get_timeout()
ranked = ping_all(hosts, port, ping_timeout)
best = ranked[0][0] if ranked else hosts[0]
save_best_host(best)
return cls(best, port, timeout, auto_reconnect, heartbeat_interval)
@staticmethod
def ping_all(
hosts: list[str] = KNOWN_HOSTS,
port: int = _DEFAULT_PORT,
hosts: list[str] | None = None,
port: int | None = None,
timeout: float = 5.0,
) -> list[tuple[str, float]]:
"""测量多台服务器延迟,返回按延迟排序的 (host, seconds) 列表。"""
if hosts is None:
hosts = get_known_hosts()
if port is None:
port = get_port()
return ping_all(hosts, port, timeout)
async def connect(self) -> None:
@@ -805,17 +869,24 @@ class AsyncTdxClient:
pass
async def _execute(self, cmd: "BaseCommand[_T]") -> _T:
"""执行命令;断线时尝试重连一次再重试(若 auto_reconnect=True"""
"""执行命令;断线时指数退避重试"""
async with self._execute_lock:
try:
return await self._conn.execute(cmd)
except TdxConnectionError:
if not self._auto_reconnect:
raise
await self._conn.close()
self._conn = AsyncTdxConnection(self._host, self._port, self._timeout)
await self._conn.connect()
return await self._conn.execute(cmd)
last_exc: TdxConnectionError | None = None
for delay in _RETRY_DELAYS:
await asyncio.sleep(delay)
await self._conn.close()
self._conn = AsyncTdxConnection(self._host, self._port, self._timeout)
await self._conn.connect()
try:
return await self._conn.execute(cmd)
except TdxConnectionError as e:
last_exc = e
raise last_exc # type: ignore[misc]
async def get_security_count(self, market: Market) -> int:
return await self._execute(GetSecurityCountCmd(market))
@@ -1025,18 +1096,24 @@ class AsyncTdxClient:
finally:
await conn.close()
async def get_financial_file_list(self, host: str = CALC_HOSTS[0]) -> pd.DataFrame:
async def get_financial_file_list(self, host: str | None = None) -> pd.DataFrame:
"""获取可用的历史专业财报文件列表(异步)。"""
if host is None:
host = get_calc_hosts()[0]
data = await self._async_download_from_host(host, "tdxfin/gpcw.txt")
raw_list = parse_financial_file_list(data)
return _to_df([FinancialFileInfo(filename=f, hash=h, filesize=s) for f, h, s in raw_list])
async def get_financial_file(self, filename: str, host: str = CALC_HOSTS[0]) -> bytes:
async def get_financial_file(self, filename: str, host: str | None = None) -> bytes:
"""从计算服务器下载财报 zip 文件(异步)。"""
if host is None:
host = get_calc_hosts()[0]
return await self._async_download_from_host(host, filename)
async def get_financial_records(self, filename: str, host: str = CALC_HOSTS[0]) -> pd.DataFrame:
async def get_financial_records(self, filename: str, host: str | None = None) -> pd.DataFrame:
"""下载财报 zip 并解析为记录列表(异步)。"""
if host is None:
host = get_calc_hosts()[0]
import io
import re
import zipfile
@@ -1057,7 +1134,7 @@ class AsyncTdxClient:
raw_records = parse_financial_dat(dat_data, report_date)
records: list[FinancialRecord] = []
for code, market_byte, rdate, fields in raw_records:
market = Market.SH if market_byte == b"\x01" else Market.SZ
market = Market.SH if market_byte == 1 else Market.SZ
records.append(
FinancialRecord(code=code, market=market, report_date=rdate, fields=fields)
)
+489
View File
@@ -0,0 +1,489 @@
"""MAC 协议字段位图编解码。
提供 FieldBit 定义、预定义字段集合(PresetField)、字段选择器(FieldSelection),
以及 20 字节请求位图的构建与响应位图解析。
"""
from collections.abc import Iterable, Iterator
from enum import Enum, IntEnum
from typing import TypeAlias
# ── 统一的字段选择类型 ──
Fields: TypeAlias = "FieldBit | PresetField | FieldSelection | Iterable[FieldBit]"
class FieldBit(IntEnum):
"""字段位定义,自带格式和描述,单一数据源。"""
fmt: str # 由 __new__ 设置
desc: str # 由 __new__ 设置
def __new__(cls, value: int, fmt: str = "<f", desc: str = "") -> "FieldBit":
obj = int.__new__(cls, value)
obj._value_ = value
obj.fmt = fmt
obj.desc = desc
return obj
@property
def field_name(self) -> str:
"""返回英文字段名,用于 DataFrame 列名等。"""
return self.name.lower()
# ── 基础字段 (0x00-0x05) ──
PRE_CLOSE = 0x00, "<f", "昨收"
OPEN = 0x01, "<f", "开盘价"
HIGH = 0x02, "<f", "最高价"
LOW = 0x03, "<f", "最低价"
CLOSE = 0x04, "<f", "收盘价"
VOL = 0x05, "<I", "成交量"
VOL_RATIO = 0x06, "<f", "量比"
AMOUNT = 0x07, "<f", "总金额(元)"
# ── 扩展字段 (0x08-0x0F) ──
INSIDE_VOLUME = 0x08, "<I", "内盘"
OUTSIDE_VOLUME = 0x09, "<I", "外盘"
TOTAL_SHARES = 0x0A, "<f", "总股数(单位万)"
FLOAT_SHARES = 0x0B, "<f", "流通股(单位万)"
EPS = 0x0C, "<f", "每股收益"
NET_ASSETS = 0x0D, "<f", "净资产"
SECURITY_TYPE_PRICE = 0x0E, "<f", "证券类型价"
TOTAL_MARKET_CAP_AB = 0x0F, "<f", "AB股总市值"
# ── 0x10-0x1F ──
PE_DYNAMIC = 0x10, "<f", "市盈率(动)"
BID_PRICE = 0x11, "<f", "买一价"
ASK_PRICE = 0x12, "<f", "卖一价"
SERVER_UPDATE_DATE = 0x13, "<I", "服务器更新日期 YYYYMMDD"
SERVER_UPDATE_TIME = 0x14, "<I", "服务器更新时间 HHMMSS"
LOT_SIZE_INFO = 0x15, "<I", "未确定"
BOARD_STRENGTH = 0x16, "<f", "板块强度(涨跌家数差)"
DIVIDEND_YIELD = 0x17, "<f", "每股股息(元)"
BID_VOLUME = 0x18, "<I", "买量"
ASK_VOLUME = 0x19, "<I", "卖量"
LAST_VOLUME = 0x1A, "<I", "现量"
TURNOVER = 0x1B, "<f", "换手"
INDUSTRY = 0x1C, "<I", "行业分类代码"
INDUSTRY_CHANGE_UP = 0x1D, "<f", "行业涨跌幅"
STOCK_TAG_FLAGS = 0x1E, "<I", "股票标签位图"
DECIMAL_POINT = 0x1F, "<I", "数据精度"
# ── 0x20-0x2F ──
BUY_PRICE_LIMIT = 0x20, "<f", "涨停价"
SELL_PRICE_LIMIT = 0x21, "<f", "跌停价"
PRICE_DECIMAL_INFO = 0x22, "<I", "价格精度标志"
LOT_SIZE = 0x23, "<I", "所属地区板块/每手股数"
PRE_IOPV = 0x24, "<f", "昨IOPV"
SPEED_PCT = 0x25, "<f", "涨速"
AVG_PRICE = 0x26, "<f", "均价"
IOPV = 0x27, "<f", "IOPV"
PE_TTM_VOL_RELATED = 0x28, "<f", "前参考价(美股适用)"
EX_PRICE_PLACEHOLDER = 0x29, "<f", "前金额参考"
OPERATING_REVENUE = 0x2A, "<f", "营业收入(万)"
FLAG_KCB = 0x2B, "<I", "科创板标志"
FLAG_BJ = 0x2C, "<I", "北交所标志"
CIRCULATING_CAPITAL_Z = 0x2D, "<f", "流通股本Z(单位:万股)"
AFTER_HOURS_VOLUME = 0x2E, "<i", "盘后量"
# ── 0x30-0x3F ──
PE_TTM = 0x30, "<f", "市盈率TTM"
PE_STATIC = 0x31, "<f", "市盈率静"
INDEX_METRIC = 0x37, "<f", "指数指标"
MAIN_NET_AMOUNT = 0x38, "<f", "今日主力净流入"
BID_ASK_RATIO = 0x39, "<f", "委比"
NON_INDEX_FLAG = 0x3A, "<I", "非指数标志"
CHANGE_20D_PCT = 0x3B, "<f", "20日涨幅%"
YTD_PCT = 0x3C, "<f", "年初至今%"
STOCK_CLASS_CODE = 0x3E, "<I", "证券子分类码"
PERCENT_BASE = 0x3F, "<I", "百分比基底"
# ── 0x40-0x4F ──
MTD_PCT = 0x40, "<f", "月初至今%"
CHANGE_1Y_PCT = 0x41, "<f", "一年涨幅%"
PREV_CHANGE_PCT = 0x42, "<f", "昨涨幅%"
CHANGE_3D_PCT = 0x43, "<f", "3日涨幅%"
CHANGE_60D_PCT = 0x44, "<f", "60日涨幅%"
CHANGE_5D_PCT = 0x45, "<f", "5日涨幅%"
CHANGE_10D_PCT = 0x46, "<f", "10日涨幅%"
PREV2_CHANGE_PCT = 0x47, "<f", "前日涨幅%"
BID2_PRICE = 0x48, "<f", "买二价"
ASK2_PRICE = 0x49, "<f", "卖二价"
AH_CODE = 0x4A, "<I", "对应A/H股code"
UNKNOWN_CODE = 0x4B, "<I", "少部分有数据"
# ── 0x50-0x6F ──
OPEN_AMOUNT = 0x57, "<f", "开盘金额(元)"
ANNUAL_LIMIT_UP_DAYS = 0x58, "<i", "年涨停天数"
ACTIVITY = 0x59, "<I", "活跃度"
DIVIDEND_YIELD_RATE = 0x5B, "<f", "股息率%"
CONSECUTIVE_UP_DAYS = 0x5C, "<i", "连涨天"
LIMIT_UP_COUNT = 0x5D, "<I", "涨停数(板块) / 买二量(个股)"
BID2_VOLUME = 0x5D, "<I", "买二量(个股)"
LIMIT_DOWN_COUNT = 0x5E, "<I", "跌停数(板块) / 卖二量(个股)"
ASK2_VOLUME = 0x5E, "<I", "卖二量(个股)"
INDUSTRY_SUB = 0x5F, "<I", "行业二级分类"
AUCTION_BUY_LIMIT = 0x66, "<f", "连续竞价买入上限"
AUCTION_SELL_LIMIT = 0x67, "<f", "连续竞价卖出下限"
VOL_SPEED_PCT = 0x68, "<f", "量涨速%"
SHORT_TURNOVER_PCT = 0x69, "<f", "短换手%"
AMOUNT_2M = 0x6A, "<f", "2分钟金额(元)"
MAIN_NET_AMOUNT_COPY = 0x6B, "<f", "今日主力净流入(副本)"
MAIN_NET_RATIO = 0x6C, "<f", "主力净比%"
RETAIL_NET_AMOUNT = 0x6D, "<f", "散户单增比"
MAIN_NET_5M_AMOUNT = 0x6E, "<f", "5分钟主力净额"
MAIN_NET_3D_AMOUNT = 0x6F, "<f", "近三日主力净额"
# ── 0x70-0x7F ──
MAIN_NET_5D_AMOUNT = 0x70, "<f", "近五日主力净额"
MAIN_NET_10D_AMOUNT = 0x71, "<f", "近十日主买金额(待确定)"
MAIN_BUY_NET_AMOUNT = 0x72, "<f", "今日主买净额"
DDX = 0x73, "<f", "DDX"
DDY = 0x74, "<f", "DDY"
DDZ = 0x75, "<f", "DDZ"
DDF = 0x76, "<f", "DDF"
STOCK_FLAG_A = 0x77, "<f", "个股标志位A"
STOCK_FLAG_B = 0x78, "<f", "个股标志位B(副本)"
AUCTION_VOL_RATIO = 0x7A, "<f", "竞价昨比"
PREV_AMOUNT = 0x7B, "<f", "昨成交额(元)"
RECENT_INDICATOR = 0x7D, "<f", "近日指标提示"
# ── 0x80-0x8F ──
BID3_PRICE = 0x80, "<f", "买三价"
BID4_PRICE = 0x81, "<f", "买四价"
BID5_PRICE = 0x82, "<f", "买五价"
ASK3_PRICE = 0x83, "<f", "卖三价"
ASK4_PRICE = 0x84, "<f", "卖四价"
ASK5_PRICE = 0x85, "<f", "卖五价"
BID3_VOLUME = 0x86, "<I", "买三量"
BID4_VOLUME = 0x87, "<I", "买四量"
UP_COUNT = 0x88, "<I", "上涨家数(板块) / 买五量(个股)"
BID5_VOLUME = 0x88, "<I", "买五量(个股)"
ASK3_VOLUME = 0x89, "<I", "卖三量"
ASK4_VOLUME = 0x8A, "<I", "卖四量"
DOWN_COUNT = 0x8B, "<I", "下跌家数(板块) / 卖五量(个股)"
ASK5_VOLUME = 0x8B, "<I", "卖五量(个股)"
BID_ASK_DIFF = 0x8C, "<i", "委差"
CHANGE_UP_TYPE = 0x8D, "<i", "封板状态"
SAFETY_SCORE = 0x8E, "<f", "安全分"
HIGHLIGHT_COUNT = 0x8F, "<f", "亮点数"
# ── 0x90-0x96: 日内时间涨幅(从昨收算) ──
CHANGE_AT_1000 = 0x90, "<f", "日内涨幅% 10:00"
CHANGE_AT_1030 = 0x91, "<f", "日内涨幅% 10:30"
CHANGE_AT_1100 = 0x92, "<f", "日内涨幅% 11:00"
CHANGE_AT_1130 = 0x93, "<f", "日内涨幅% 11:30"
CHANGE_AT_1330 = 0x94, "<f", "日内涨幅% 13:30"
CHANGE_AT_1400 = 0x95, "<f", "日内涨幅% 14:00"
CHANGE_AT_1430 = 0x96, "<f", "日内涨幅% 14:30"
# 从 FieldBit 自动生成
FIELD_BITMAP_MAP: dict[int, tuple[str, str, str]] = {
bit.value: (bit.name.lower(), bit.fmt, bit.desc) for bit in FieldBit
}
# ── 字段后处理钩子 ──
def _post_ah_code(value: int, market: int = 0) -> str:
"""A/H股代码补齐位数。"""
if not value:
return ""
# 沪深北 5 位,其他 6 位
width = 5 if market in (0, 1) else 6
return str(value).zfill(width)
FIELD_POSTPROCESS: dict[int, object] = {
0x4A: _post_ah_code, # AH_CODE: 补齐0
}
# ── 控制区(位128-159, 4字节) ──
# 前16字节(位0-127)是字段位图, 后4字节(位128-159)是控制区:
# 字节16(位128-135): 盘口深度(bid3_price~bid4_volume)
# 字节17(位136-143): 排除/限流位
# 字节18(位144-151): 日内涨幅(change_at_1000~1430)
# 字节19(位152-159): 控制字节(CTRL_EXTENDED等)
CTRL_BYTE = 0 # 控制字节起始位(152)
CTRL_EXTENDED = 1 # 非0=扩展模式(含北交所等),0=标准模式(仅A股)
# ── 预定义字段集合 ──
class PresetField(Enum):
"""预定义字段集合,支持 + / | 链式组合。
Usage:
PresetField.BASIC + PresetField.VOLUME # 两个预设合并
PresetField.OHLC + FieldBit.AH_CODE # 预设 + 单字段
FieldBit.OPEN + FieldBit.HIGH + FieldBit.LOW # 纯字段组合
"""
NONE = ()
OHLC = (FieldBit.OPEN, FieldBit.HIGH, FieldBit.LOW, FieldBit.CLOSE)
BASIC = (
FieldBit.OPEN,
FieldBit.HIGH,
FieldBit.LOW,
FieldBit.CLOSE,
FieldBit.PRE_CLOSE,
FieldBit.VOL,
)
QUOTE = (
FieldBit.BID_PRICE,
FieldBit.ASK_PRICE,
FieldBit.BID_VOLUME,
FieldBit.ASK_VOLUME,
FieldBit.LAST_VOLUME,
)
VOLUME = (FieldBit.VOL, FieldBit.AMOUNT, FieldBit.TURNOVER, FieldBit.VOL_RATIO)
FUNDAMENTAL = (
FieldBit.TOTAL_SHARES,
FieldBit.FLOAT_SHARES,
FieldBit.EPS,
FieldBit.NET_ASSETS,
)
ENHANCED = (
FieldBit.OPEN,
FieldBit.HIGH,
FieldBit.LOW,
FieldBit.CLOSE,
FieldBit.VOL,
FieldBit.FLOAT_SHARES,
FieldBit.ACTIVITY,
)
AH_CODE_FIELDS = (
FieldBit.OPEN,
FieldBit.HIGH,
FieldBit.LOW,
FieldBit.CLOSE,
FieldBit.VOL,
FieldBit.AH_CODE,
FieldBit.LOT_SIZE,
FieldBit.INDUSTRY,
)
BOARD_STATS = (
FieldBit.LIMIT_UP_COUNT,
FieldBit.LIMIT_DOWN_COUNT,
FieldBit.UP_COUNT,
FieldBit.DOWN_COUNT,
)
HANDICAP = (
FieldBit.BID_PRICE,
FieldBit.BID2_PRICE,
FieldBit.BID3_PRICE,
FieldBit.BID4_PRICE,
FieldBit.BID5_PRICE,
FieldBit.ASK_PRICE,
FieldBit.ASK2_PRICE,
FieldBit.ASK3_PRICE,
FieldBit.ASK4_PRICE,
FieldBit.ASK5_PRICE,
FieldBit.BID_VOLUME,
FieldBit.BID2_VOLUME,
FieldBit.BID3_VOLUME,
FieldBit.BID4_VOLUME,
FieldBit.BID5_VOLUME,
FieldBit.ASK_VOLUME,
FieldBit.ASK2_VOLUME,
FieldBit.ASK3_VOLUME,
FieldBit.ASK4_VOLUME,
FieldBit.ASK5_VOLUME,
)
COMMON = (
FieldBit.PRE_CLOSE,
FieldBit.OPEN,
FieldBit.HIGH,
FieldBit.LOW,
FieldBit.CLOSE,
FieldBit.VOL,
FieldBit.VOL_RATIO,
FieldBit.AMOUNT,
FieldBit.TOTAL_SHARES,
FieldBit.FLOAT_SHARES,
FieldBit.EPS,
FieldBit.NET_ASSETS,
FieldBit.SECURITY_TYPE_PRICE,
FieldBit.TOTAL_MARKET_CAP_AB,
FieldBit.PE_DYNAMIC,
FieldBit.LOT_SIZE_INFO,
FieldBit.DIVIDEND_YIELD,
FieldBit.LAST_VOLUME,
FieldBit.TURNOVER,
FieldBit.STOCK_TAG_FLAGS,
FieldBit.DECIMAL_POINT,
FieldBit.BUY_PRICE_LIMIT,
FieldBit.SELL_PRICE_LIMIT,
FieldBit.PRICE_DECIMAL_INFO,
FieldBit.LOT_SIZE,
FieldBit.PRE_IOPV,
FieldBit.SPEED_PCT,
FieldBit.FLAG_KCB,
FieldBit.PE_TTM,
FieldBit.PE_STATIC,
FieldBit.MAIN_NET_AMOUNT,
FieldBit.VOL_SPEED_PCT,
FieldBit.SHORT_TURNOVER_PCT,
FieldBit.CIRCULATING_CAPITAL_Z,
)
DEBUG = (-1, "", "调试用全字段")
ALL = tuple(FieldBit)
def __add__(self, other: object) -> "FieldSelection":
if isinstance(other, FieldBit | PresetField | FieldSelection):
return FieldSelection(self, other)
return NotImplemented
def __or__(self, other: object) -> "FieldSelection":
return self.__add__(other)
def __radd__(self, other: object) -> "FieldSelection":
if isinstance(other, FieldBit | FieldSelection):
return FieldSelection(other, self)
return NotImplemented
def __ror__(self, other: object) -> "FieldSelection":
return self.__radd__(other)
class FieldSelection:
"""字段选择器,支持 PresetField + FieldBit 组合。
Usage:
PresetField.BASIC + FieldBit.AH_CODE
PresetField.BASIC | FieldBit.INDUSTRY
FieldBit.OPEN + FieldBit.HIGH + FieldBit.LOW
"""
__slots__ = ("_fields",)
def __init__(self, *parts: "FieldBit | PresetField | FieldSelection") -> None:
seen: set[FieldBit] = set()
result: list[FieldBit] = []
for part in parts:
if isinstance(part, PresetField):
source: Iterable[FieldBit] = part.value
elif isinstance(part, FieldBit):
source = (part,)
else:
source = part._fields
for bit in source:
if bit not in seen:
seen.add(bit)
result.append(bit)
self._fields: tuple[FieldBit, ...] = tuple(result)
def __add__(self, other: object) -> "FieldSelection":
if isinstance(other, FieldBit | PresetField | FieldSelection):
return FieldSelection(self, other)
return NotImplemented
def __or__(self, other: object) -> "FieldSelection":
return self.__add__(other)
def __radd__(self, other: object) -> "FieldSelection":
if isinstance(other, FieldBit | PresetField):
return FieldSelection(other, self)
return NotImplemented
def __ror__(self, other: object) -> "FieldSelection":
return self.__radd__(other)
def __iter__(self) -> Iterator[FieldBit]:
return iter(self._fields)
def __len__(self) -> int:
return len(self._fields)
def __bool__(self) -> bool:
return bool(self._fields)
def __contains__(self, item: object) -> bool:
return item in self._fields
def __repr__(self) -> str:
names = [bit.name for bit in self._fields]
return f"FieldSelection([{', '.join(names)}])"
def normalize_fields(fields: "Fields") -> FieldSelection:
"""将任意字段选择形式归一化为 FieldSelection。"""
if fields is None:
return FieldSelection()
if isinstance(fields, FieldSelection):
return fields
if isinstance(fields, PresetField):
return FieldSelection(*fields.value)
if isinstance(fields, FieldBit):
return FieldSelection(fields)
return FieldSelection(*fields)
def build_bitmap(
fields: "Fields",
exclude_flags: int = 0,
) -> bytearray:
"""将字段选择转换为 20 字节请求位图。
Parameters
----------
fields : Fields
字段选择,可以是 PresetField、FieldBit、FieldSelection 或可迭代对象。
exclude_flags : int
控制区 4 字节(位 128-159)的值,默认 0。
Returns
-------
bytearray
20 字节位图。
"""
if isinstance(fields, PresetField) and fields is PresetField.DEBUG:
return bytearray(b"\xff" * 20)
selection = normalize_fields(fields)
bitmap_int = 0
for bit in selection:
bitmap_int |= 1 << bit.value
ba = bytearray(bitmap_int.to_bytes(16, "little"))
ba.extend(exclude_flags.to_bytes(4, "little"))
return ba
def build_exclude_flags(exclude_flags: int = 0) -> bytes:
"""构建 4 字节控制区。
Parameters
----------
exclude_flags : int
控制区原始值,默认 0。
Returns
-------
bytes
4 字节控制区。
"""
return exclude_flags.to_bytes(4, "little")
def get_active_fields(bitmap_bytes: bytes) -> list[tuple[FieldBit, str]]:
"""从响应位图解析活跃字段。
Parameters
----------
bitmap_bytes : bytes
响应中的位图字节(通常 16 或 20 字节)。
Returns
-------
list[tuple[FieldBit, str]]
活跃字段及其格式说明符,按位序升序。
"""
bitmap_int = int.from_bytes(bitmap_bytes, "little")
active: list[tuple[FieldBit, str]] = []
while bitmap_int:
lowbit = bitmap_int & -bitmap_int
bit_pos = lowbit.bit_length() - 1
bitmap_int ^= lowbit
field = FieldBit._value2member_map_.get(bit_pos)
if field is not None and isinstance(field, FieldBit):
active.append((field, field.fmt))
return active
+50
View File
@@ -0,0 +1,50 @@
"""MAC 协议请求帧构建。
MAC 协议请求帧格式(10 字节头 + body):
struct "<BIBHH"
偏移 0: B (1字节) — head_flag (MAC=0x1c, 标准=0x0c)
偏移 1: I (4字节) — customize(通常为 0
偏移 5: B (1字节) — version(通常为 1
偏移 6: H (2字节) — zipsizebody 长度)
偏移 8: H (2字节) — unzipsize(同 zipsizeMAC 不压缩请求)
MAC 响应复用标准 16 字节帧头(<IIIHH),直接使用 frame.py 的 parse_header/decompress_body。
"""
import struct
_MAC_HEADER_FMT = "<BIBHH"
_MAC_HEADER_SIZE = 10
_MAC_HEAD_FLAG = 0x1C
_MAC_CUSTOMIZE = 0
_MAC_VERSION = 1
def build_mac_request(msg_id: int, body: bytes, *, head_flag: int = _MAC_HEAD_FLAG) -> bytes:
"""构建 MAC 协议请求帧。
Parameters
----------
msg_id : int
MAC 命令 ID(如 0x122B)。
body : bytes
命令特有的请求体(不含 msg_id 前缀)。
head_flag : int
帧头标识字节,默认 0x1C(标准 MAC)。部分命令(如 0x1218)
使用不同的 head_flag 区分子协议。
Returns
-------
bytes
完整的请求帧(10 字节头 + 2 字节 msg_id + body)。
"""
inner = struct.pack("<H", msg_id) + body
header = struct.pack(
_MAC_HEADER_FMT,
head_flag,
_MAC_CUSTOMIZE,
_MAC_VERSION,
len(inner),
len(inner),
)
return header + inner
+280
View File
@@ -0,0 +1,280 @@
"""集中管理服务器地址、端口、超时等配置。
优先级:环境变量 > ~/.easy_tdx/config.json > 源码内嵌默认值。
配置文件示例::
{
"best_host": "180.153.18.170",
"best_host_updated_at": "2026-05-22T10:30:00",
"known_hosts": ["111.229.247.189", ...],
"calc_hosts": ["120.76.152.87"],
"mac_hosts": ["121.36.248.138", ...],
"port": 7709,
"timeout": 15.0
}
环境变量覆盖::
EASY_TDX_HOST -- 单台主机地址
EASY_TDX_PORT -- 端口
EASY_TDX_TIMEOUT -- 超时秒数
EASY_TDX_KNOWN_HOSTS -- 逗号分隔的候选主机列表
EASY_TDX_CONFIG_DIR -- 配置文件目录(默认 ~/.easy_tdx
"""
import json
import os
from datetime import datetime
from pathlib import Path
from typing import Any
_CONFIG_DIR = Path(os.environ.get("EASY_TDX_CONFIG_DIR", str(Path.home() / ".easy_tdx")))
_CONFIG_FILE = _CONFIG_DIR / "config.json"
# ---------------------------------------------------------------------------
# 源码内嵌默认值(config.json 不存在或字段缺失时的兜底)
# ---------------------------------------------------------------------------
_FALLBACK_HOSTS: list[str] = [
"111.229.247.189",
"150.158.160.2",
"180.153.18.170",
"124.71.187.122",
"180.153.18.171",
"180.153.18.172",
"119.147.212.81",
"115.238.56.198",
"115.238.90.165",
"218.75.126.9",
"47.107.75.159",
"59.175.238.38",
"110.41.147.114",
"110.41.2.72",
"101.33.225.16",
"175.178.112.197",
"175.178.128.227",
"43.139.95.83",
"124.223.163.242",
"122.51.120.217",
"123.60.164.122",
"124.70.199.56",
"62.234.50.143",
"81.70.151.186",
"82.156.214.79",
"159.75.29.111",
"43.139.18.171",
"81.71.32.47",
"122.51.232.182",
"118.25.98.114",
"121.36.225.169",
"123.60.70.228",
"123.60.73.44",
"124.70.133.119",
"124.71.187.72",
"119.97.185.59",
"129.204.230.128",
"101.42.240.54",
"124.71.9.153",
"123.60.84.66",
"111.230.186.52",
"101.43.159.194",
"120.53.8.251",
"152.136.191.169",
"116.205.163.254",
"116.205.171.132",
"116.205.183.150",
"49.232.15.141",
"82.156.174.84",
"101.42.164.241",
"101.35.121.35",
"111.231.113.208",
]
_FALLBACK_CALC_HOSTS: list[str] = [
"120.76.152.87",
]
_FALLBACK_MAC_HOSTS: list[str] = [
"121.36.248.138",
"123.60.47.136",
"121.37.207.165",
]
_FALLBACK_EX_HOSTS: list[str] = [
"112.74.214.43",
"120.25.218.6",
"43.139.173.246",
"159.75.90.107",
"106.52.170.195",
"139.9.191.175",
"175.24.47.69",
"150.158.9.199",
"150.158.20.127",
"49.235.119.116",
"49.234.13.160",
"116.205.143.214",
"124.71.223.19",
"113.45.175.47",
"123.60.173.210",
"118.89.69.202",
]
_FALLBACK_MAC_EX_HOSTS: list[str] = [
"116.205.135.205",
"121.37.232.167",
]
_FALLBACK_PORT = 7709
_FALLBACK_TIMEOUT = 15.0
# ---------------------------------------------------------------------------
# 内部读写
# ---------------------------------------------------------------------------
def _load() -> dict[str, Any]:
try:
if _CONFIG_FILE.exists():
return json.loads(_CONFIG_FILE.read_text("utf-8"))
except Exception:
pass
return {}
def _save(data: dict[str, Any]) -> None:
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
tmp = _CONFIG_FILE.with_suffix(".tmp")
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), "utf-8")
tmp.replace(_CONFIG_FILE)
# ---------------------------------------------------------------------------
# 公开 getter
# ---------------------------------------------------------------------------
def get_best_host() -> str:
"""返回当前最佳主机地址。优先级:环境变量 > config.json > 默认列表首个。"""
env = os.environ.get("EASY_TDX_HOST")
if env:
return env
cfg = _load()
return cfg.get("best_host", _FALLBACK_HOSTS[0])
def get_known_hosts() -> list[str]:
"""返回候选行情主机列表。"""
env = os.environ.get("EASY_TDX_KNOWN_HOSTS")
if env:
return [h.strip() for h in env.split(",") if h.strip()]
cfg = _load()
return cfg.get("known_hosts", list(_FALLBACK_HOSTS))
def get_calc_hosts() -> list[str]:
"""返回计算服务器列表。"""
cfg = _load()
return cfg.get("calc_hosts", list(_FALLBACK_CALC_HOSTS))
def get_mac_hosts() -> list[str]:
"""返回 MAC 行情服务器列表。"""
cfg = _load()
return cfg.get("mac_hosts", list(_FALLBACK_MAC_HOSTS))
def get_ex_hosts() -> list[str]:
"""返回扩展行情服务器列表。"""
cfg = _load()
return cfg.get("ex_hosts", list(_FALLBACK_EX_HOSTS))
def get_best_ex_host() -> str:
"""返回当前最佳扩展行情主机。"""
env = os.environ.get("EASY_TDX_EX_HOST")
if env:
return env
cfg = _load()
return cfg.get("best_ex_host", _FALLBACK_EX_HOSTS[0])
def get_mac_ex_hosts() -> list[str]:
"""返回 MAC 协议扩展行情服务器列表。"""
cfg = _load()
return cfg.get("mac_ex_hosts", list(_FALLBACK_MAC_EX_HOSTS))
def get_best_mac_ex_host() -> str:
"""返回当前最佳 MAC 协议扩展行情主机。"""
env = os.environ.get("EASY_TDX_MAC_EX_HOST")
if env:
return env
cfg = _load()
return cfg.get("best_mac_ex_host", _FALLBACK_MAC_EX_HOSTS[0])
def get_port() -> int:
"""返回默认端口。"""
env = os.environ.get("EASY_TDX_PORT")
if env:
return int(env)
cfg = _load()
return cfg.get("port", _FALLBACK_PORT)
def get_timeout() -> float:
"""返回默认超时秒数。"""
env = os.environ.get("EASY_TDX_TIMEOUT")
if env:
return float(env)
cfg = _load()
return cfg.get("timeout", _FALLBACK_TIMEOUT)
# ---------------------------------------------------------------------------
# 持久化
# ---------------------------------------------------------------------------
def save_best_host(host: str) -> None:
"""保存最佳主机到配置文件;首次写入时同时补全默认配置。"""
cfg = _load()
cfg["best_host"] = host
cfg["best_host_updated_at"] = datetime.now().isoformat()
if "known_hosts" not in cfg:
cfg["known_hosts"] = list(_FALLBACK_HOSTS)
if "calc_hosts" not in cfg:
cfg["calc_hosts"] = list(_FALLBACK_CALC_HOSTS)
if "mac_hosts" not in cfg:
cfg["mac_hosts"] = list(_FALLBACK_MAC_HOSTS)
if "port" not in cfg:
cfg["port"] = _FALLBACK_PORT
if "ex_hosts" not in cfg:
cfg["ex_hosts"] = list(_FALLBACK_EX_HOSTS)
if "mac_ex_hosts" not in cfg:
cfg["mac_ex_hosts"] = list(_FALLBACK_MAC_EX_HOSTS)
_save(cfg)
def save_best_ex_host(host: str) -> None:
"""保存最佳扩展行情主机到配置文件。"""
cfg = _load()
cfg["best_ex_host"] = host
cfg["best_ex_host_updated_at"] = datetime.now().isoformat()
if "ex_hosts" not in cfg:
cfg["ex_hosts"] = list(_FALLBACK_EX_HOSTS)
if "mac_ex_hosts" not in cfg:
cfg["mac_ex_hosts"] = list(_FALLBACK_MAC_EX_HOSTS)
_save(cfg)
def save_best_mac_ex_host(host: str) -> None:
"""保存最佳 MAC 协议扩展行情主机到配置文件。"""
cfg = _load()
cfg["best_mac_ex_host"] = host
cfg["best_mac_ex_host_updated_at"] = datetime.now().isoformat()
if "mac_ex_hosts" not in cfg:
cfg["mac_ex_hosts"] = list(_FALLBACK_MAC_EX_HOSTS)
_save(cfg)
+5 -1
View File
@@ -1,11 +1,15 @@
"""easy_tdx.ex — 通达信扩展行情(期货、港股、外股等,端口 7727)。"""
from .client import AsyncExTdxClient, ExTdxClient
from .models import KNOWN_EX_HOSTS, KNOWN_EX_MARKETS
from .mac_client import AsyncMacExClient, MacExClient
from .models import KNOWN_EX_HOSTS, KNOWN_EX_MARKETS, MAC_EX_HOSTS
__all__ = [
"ExTdxClient",
"AsyncExTdxClient",
"MacExClient",
"AsyncMacExClient",
"KNOWN_EX_HOSTS",
"KNOWN_EX_MARKETS",
"MAC_EX_HOSTS",
]
+16 -10
View File
@@ -6,6 +6,7 @@ from types import TracebackType
from typing import TypeVar
from ..commands.base import BaseCommand
from ..config import get_best_ex_host, get_ex_hosts, save_best_ex_host
from ..exceptions import TdxConnectionError
from .commands.get_history_bars_range import GetExHistoryInstrumentBarsRangeCmd
from .commands.get_instrument_bars import GetExInstrumentBarsCmd
@@ -23,7 +24,6 @@ from .commands.get_transaction import (
GetExTransactionDataCmd,
)
from .models import (
KNOWN_EX_HOSTS,
ExInstrumentBar,
ExInstrumentInfo,
ExInstrumentQuote,
@@ -55,16 +55,16 @@ class ExTdxClient:
def __init__(
self,
host: str = KNOWN_EX_HOSTS[0],
host: str | None = None,
port: int = _DEFAULT_EX_PORT,
timeout: float = 15.0,
auto_reconnect: bool = True,
) -> None:
self._host = host
self._host = host if host is not None else get_best_ex_host()
self._port = port
self._timeout = timeout
self._auto_reconnect = auto_reconnect
self._conn = ExTdxConnection(host, port, timeout)
self._conn = ExTdxConnection(self._host, port, timeout)
@classmethod
def from_best_host(
@@ -75,9 +75,12 @@ class ExTdxClient:
ping_timeout: float = 5.0,
auto_reconnect: bool = True,
) -> "ExTdxClient":
"""测量所有扩展行情服务器延迟,选最低延迟建立连接。"""
"""测量所有扩展行情服务器延迟,选最低延迟建立连接。自动保存最佳主机。"""
if hosts is None:
hosts = get_ex_hosts()
ranked = ping_ex_all(hosts, port, ping_timeout)
best = ranked[0][0] if ranked else (hosts or KNOWN_EX_HOSTS)[0]
best = ranked[0][0] if ranked else hosts[0]
save_best_ex_host(best)
return cls(best, port, timeout, auto_reconnect)
@staticmethod
@@ -239,18 +242,18 @@ class AsyncExTdxClient:
def __init__(
self,
host: str = KNOWN_EX_HOSTS[0],
host: str | None = None,
port: int = _DEFAULT_EX_PORT,
timeout: float = 15.0,
auto_reconnect: bool = True,
heartbeat_interval: float = 60.0,
) -> None:
self._host = host
self._host = host if host is not None else get_best_ex_host()
self._port = port
self._timeout = timeout
self._auto_reconnect = auto_reconnect
self._heartbeat_interval = heartbeat_interval
self._conn = AsyncExTdxConnection(host, port, timeout)
self._conn = AsyncExTdxConnection(self._host, port, timeout)
self._execute_lock = asyncio.Lock()
self._heartbeat_task: asyncio.Task[None] | None = None
@@ -264,8 +267,11 @@ class AsyncExTdxClient:
auto_reconnect: bool = True,
heartbeat_interval: float = 60.0,
) -> "AsyncExTdxClient":
if hosts is None:
hosts = get_ex_hosts()
ranked = ping_ex_all(hosts, port, ping_timeout)
best = ranked[0][0] if ranked else (hosts or KNOWN_EX_HOSTS)[0]
best = ranked[0][0] if ranked else hosts[0]
save_best_ex_host(best)
return cls(best, port, timeout, auto_reconnect, heartbeat_interval)
@staticmethod
@@ -14,4 +14,4 @@ class GetExInstrumentCountCmd(BaseCommand[int]):
if len(body) < 23:
return 0
(count,) = unpack_from("<I", body, 19, "ex instrument count")
return count
return int(count)
+49
View File
@@ -0,0 +1,49 @@
"""MAC EX 扩展行情登录命令(msg_id=0x2454)。
MAC EX 服务器(端口 7727)在数据查询前要求先完成 Login,
否则后续所有命令都会被服务器断开连接。
"""
import struct
from ...commands.base import BaseCommand
_MSG_ID = 0x2454
_HEAD_FLAG = 0x01
# 80 字节 Login body,来自 opentdx 参考实现,已通过实际测试验证。
_LOGIN_BODY = bytes(bytearray.fromhex(
"e5bb1c2fafe52594"
"1f32c6e5d53dfb41"
"5b734cc9cdbf0ac9"
"2021bfdd1eb06d22"
"d008884c1611cb13"
"78f6abd824d899d2"
"1f32c6e5d53dfb41"
"1f32c6e5d53dfb41"
"a9325ac935dc0837"
"335a16e4ce17c1bb"
))
# EX 协议帧头格式: head_flag(1B) + customize(4B) + version(1B) + zipsize(2B) + unzipsize(2B)
_EX_HEADER_FMT = "<BIBHH"
class MacExLoginCmd(BaseCommand[bool]):
"""MAC EX 扩展行情登录命令。"""
def build_request(self) -> bytes:
inner = struct.pack("<H", _MSG_ID) + _LOGIN_BODY
header = struct.pack(
_EX_HEADER_FMT,
_HEAD_FLAG,
0, # customize
1, # version
len(inner),
len(inner),
)
return header + inner
def parse_response(self, body: bytes) -> bool:
# Login 响应 body 非空即视为成功
return len(body) >= 2
+715
View File
@@ -0,0 +1,715 @@
"""MAC 协议扩展市场高层 APIMacExClient(同步)和 AsyncMacExClientasyncio)。
期货/港股/美股等扩展市场通过 MAC 协议命令(0x122B/0x122E/0x122D/0x122F/0x2562
获取数据,使用 ExTdxConnection(端口 7727,单包握手)。
"""
import asyncio
from datetime import date
from types import TracebackType
from typing import Any, TypeVar
import pandas as pd
from .._df import _to_df
from ..commands.base import BaseCommand
from ..exceptions import TdxConnectionError
from .commands.login import MacExLoginCmd
from .commands.get_instrument_count import GetExInstrumentCountCmd
from .commands.get_instrument_info import GetExInstrumentInfoCmd
from ..mac.commands.chart_sampling import ChartSamplingCmd
from ..mac.commands.symbol_bar import SymbolBarCmd
from ..mac.commands.symbol_quotes import SymbolQuotesCmd
from ..mac.commands.symbol_tick_chart import SymbolTickChartCmd
from ..mac.commands.symbol_transaction import SymbolTransactionCmd
from ..mac.enums import Adjust, Period, SortOrder, SortType
from ..config import get_best_mac_ex_host, get_mac_ex_hosts, save_best_mac_ex_host
from ..mac.models import MacQuoteField
from .transport.async_ import AsyncExTdxConnection
from .transport.sync import ExTdxConnection, ping_ex_all
_DEFAULT_PORT = 7727
_T = TypeVar("_T")
def _quotes_to_df(result: list[MacQuoteField]) -> pd.DataFrame:
"""将 MacQuoteField 列表展开为 DataFrame。"""
rows: list[dict[str, Any]] = []
for item in result:
row: dict[str, Any] = {"market": item.market, "code": item.code, "name": item.name}
row.update(item.fields)
rows.append(row)
return pd.DataFrame(rows) if rows else pd.DataFrame()
# ============================================================
# 同步客户端
# ============================================================
class MacExClient:
"""同步 MAC 协议扩展市场客户端(期货/港股/美股,端口 7727)。
使用示例::
with MacExClient() as c:
df = c.goods_kline(ExMarket.CFFEX_FUTURES, "IFL0", Period.DAILY)
df = c.goods_quotes([(ExMarket.HK_MAIN_BOARD, "00700")])
"""
def __init__(
self,
host: str | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
auto_reconnect: bool = True,
) -> None:
self._host = host if host is not None else get_best_mac_ex_host()
self._port = port
self._timeout = timeout
self._auto_reconnect = auto_reconnect
self._conn = ExTdxConnection(self._host, port, timeout, mac_ex_mode=True)
@classmethod
def from_best_host(
cls,
hosts: list[str] | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
ping_timeout: float = 5.0,
auto_reconnect: bool = True,
) -> "MacExClient":
"""测量所有 MAC 扩展行情服务器延迟,选最低延迟建立连接。"""
candidates = hosts or get_mac_ex_hosts()
ranked = ping_ex_all(candidates, port, ping_timeout)
best = ranked[0][0] if ranked else candidates[0]
save_best_mac_ex_host(best)
return cls(best, port, timeout, auto_reconnect)
@staticmethod
def ping_all(
hosts: list[str] | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 5.0,
) -> list[tuple[str, float]]:
return ping_ex_all(hosts or get_mac_ex_hosts(), port, timeout)
# ------------------------------------------------------------------ #
# 连接管理
# ------------------------------------------------------------------ #
def connect(self) -> None:
self._conn.connect()
self._login()
def close(self) -> None:
self._conn.close()
def disconnect(self) -> None:
self.close()
def ensure_connected(self) -> None:
"""验证连接存活,断线则自动重建。"""
try:
self._execute(GetExInstrumentCountCmd())
except TdxConnectionError:
self._conn.close()
self._conn = ExTdxConnection(self._host, self._port, self._timeout, mac_ex_mode=True)
self._conn.connect()
self._login()
def __enter__(self) -> "MacExClient":
self.connect()
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.close()
def _login(self) -> None:
"""执行 MAC EX 登录命令。"""
self._conn.execute(MacExLoginCmd())
def _execute(self, cmd: "BaseCommand[_T]") -> _T:
try:
return self._conn.execute(cmd)
except TdxConnectionError:
if not self._auto_reconnect:
raise
self._conn.close()
self._conn = ExTdxConnection(self._host, self._port, self._timeout, mac_ex_mode=True)
self._conn.connect()
self._login()
return self._conn.execute(cmd)
# ------------------------------------------------------------------ #
# 商品列表
# ------------------------------------------------------------------ #
def goods_count(self, market: int | None = None) -> int:
"""获取商品总数。market=None 时返回全市场总数,否则返回指定市场的数量。"""
if market is None:
return self._execute(GetExInstrumentCountCmd())
# 需要二分查找定位市场边界来计数
offset = self._find_market_offset(market)
if offset < 0:
return 0
total = self._execute(GetExInstrumentCountCmd())
# 从 offset 开始扫描计数
n = 0
page = 1000
pos = offset
while pos < total:
batch = self._execute(GetExInstrumentInfoCmd(start=pos, count=page))
if not batch:
break
for item in batch:
if item.market == market:
n += 1
elif item.market > market:
return n
pos += page
return n
def goods_list(self, market: int, start: int = 0, count: int = 600) -> pd.DataFrame:
"""获取扩展市场商品列表(期货合约/港股/美股等)。
通过 EX 协议的 GetInstrumentInfo 命令获取,按 market 过滤。
Parameters
----------
market : int
ExMarket 枚举值,如 ExMarket.HK_MAIN_BOARD。
start : int
市场内起始偏移。
count : int
请求数量。
"""
offset = self._find_market_offset(market)
if offset < 0:
return pd.DataFrame()
total = self._execute(GetExInstrumentCountCmd())
page_size = 1000
collected: list = []
skipped = 0
pos = offset
while pos < total and len(collected) < count:
batch = self._execute(GetExInstrumentInfoCmd(start=pos, count=page_size))
if not batch:
break
for item in batch:
if item.market == market:
if skipped < start:
skipped += 1
else:
collected.append(item)
if len(collected) >= count:
break
elif item.market > market:
break
else:
pos += page_size
continue
break
return _to_df(collected)
def _find_market_offset(self, market: int) -> int:
"""二分查找定位指定市场在全局商品列表中的起始偏移。"""
total = self._execute(GetExInstrumentCountCmd())
if total == 0:
return -1
lo, hi = 0, total
while lo < hi:
mid = (lo + hi) // 2
items = self._execute(GetExInstrumentInfoCmd(start=mid, count=1))
if not items:
hi = mid
continue
m = items[0].market
if m < market:
lo = mid + 1
else:
hi = mid
return lo
# ------------------------------------------------------------------ #
# 行情
# ------------------------------------------------------------------ #
def goods_quotes(
self,
stocks: list[tuple[int, str]],
fields: Any = None,
) -> pd.DataFrame:
"""批量获取扩展市场自定义字段报价。
Parameters
----------
stocks : list[tuple[int, str]]
[(ExMarketcode, code), ...] 列表,最多 80 只。
fields : Fields | None
字段选择,默认 PresetField.COMMON。
"""
cmd = SymbolQuotesCmd(stocks, fields)
result: list[MacQuoteField] = self._execute(cmd)
return _quotes_to_df(result)
def goods_quotes_list(
self,
market: int,
start: int = 0,
count: int = 100,
sort_type: SortType = SortType.CODE,
sort_order: SortOrder = SortOrder.NONE,
) -> pd.DataFrame:
"""获取扩展市场排序报价列表(通过 GoodsList + Quotes 组合)。
先获取商品列表,再批量查询报价。
Parameters
----------
market : int
ExMarket 枚举值。
start : int
起始偏移。
count : int
返回条数(最大 80,受报价批量限制)。
sort_type : SortType
排序字段(暂未实现排序,预留接口)。
sort_order : SortOrder
排序方向(暂未实现排序,预留接口)。
"""
page_size = min(count, 80)
items_df = self.goods_list(market, start=start, count=page_size)
if items_df.empty:
return pd.DataFrame()
stocks: list[tuple[int, str]] = []
for _, row in items_df.iterrows():
stocks.append((market, row["code"]))
cmd = SymbolQuotesCmd(stocks)
result: list[MacQuoteField] = self._execute(cmd)
return _quotes_to_df(result)
def goods_kline(
self,
market: int,
code: str,
period: Period = Period.DAILY,
start: int = 0,
count: int = 800,
adjust: Adjust = Adjust.NONE,
) -> pd.DataFrame:
"""获取扩展市场 K 线数据(支持复权)。
Parameters
----------
market : int
ExMarket 枚举值。
code : str
证券代码。
period : Period
K 线周期。
start : int
起始偏移(0=最新)。
count : int
返回条数。
adjust : Adjust
复权方式(NONE/QFQ/HFQ)。
"""
cmd = SymbolBarCmd(
market=market,
code=code,
period=period,
start=start,
count=count,
fq=adjust,
)
result = self._execute(cmd)
return _to_df(result)
# ------------------------------------------------------------------ #
# 分时
# ------------------------------------------------------------------ #
def goods_tick_chart(
self,
market: int,
code: str,
query_date: date | None = None,
) -> pd.DataFrame:
"""获取单日分时图。
Parameters
----------
market : int
ExMarket 枚举值。
code : str
证券代码。
query_date : date | None
查询日期,None 表示今天。
"""
cmd = SymbolTickChartCmd(market=market, code=code, query_date=query_date)
result = self._execute(cmd)
return _to_df(result)
def goods_chart_sampling(
self,
market: int,
code: str,
) -> pd.DataFrame:
"""获取分时缩略采样价格点(约 240 个点)。
Parameters
----------
market : int
ExMarket 枚举值。
code : str
证券代码。
"""
cmd = ChartSamplingCmd(market=market, code=code)
prices: list[float] = self._execute(cmd)
if not prices:
return pd.DataFrame()
return pd.DataFrame({"price": prices})
# ------------------------------------------------------------------ #
# 成交
# ------------------------------------------------------------------ #
def goods_transaction(
self,
market: int,
code: str,
query_date: date | None = None,
start: int = 0,
count: int = 2000,
) -> pd.DataFrame:
"""获取逐笔成交数据。
Parameters
----------
market : int
ExMarket 枚举值。
code : str
证券代码。
query_date : date | None
查询日期,None 表示今天。
start : int
起始偏移。
count : int
返回条数。
"""
cmd = SymbolTransactionCmd(
market=market,
code=code,
query_date=query_date,
start=start,
count=count,
)
result = self._execute(cmd)
return _to_df(result)
# ============================================================
# 异步客户端
# ============================================================
class AsyncMacExClient:
"""异步 MAC 协议扩展市场客户端(asyncio,端口 7727)。
使用示例::
async with AsyncMacExClient() as c:
df = await c.goods_kline(ExMarket.CFFEX_FUTURES, "IFL0", Period.DAILY)
"""
def __init__(
self,
host: str | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
auto_reconnect: bool = True,
heartbeat_interval: float = 60.0,
) -> None:
self._host = host if host is not None else get_best_mac_ex_host()
self._port = port
self._timeout = timeout
self._auto_reconnect = auto_reconnect
self._heartbeat_interval = heartbeat_interval
self._conn = AsyncExTdxConnection(self._host, port, timeout, mac_ex_mode=True)
self._execute_lock = asyncio.Lock()
self._heartbeat_task: asyncio.Task[None] | None = None
@classmethod
def from_best_host(
cls,
hosts: list[str] | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
ping_timeout: float = 5.0,
auto_reconnect: bool = True,
heartbeat_interval: float = 60.0,
) -> "AsyncMacExClient":
candidates = hosts or get_mac_ex_hosts()
ranked = ping_ex_all(candidates, port, ping_timeout)
best = ranked[0][0] if ranked else candidates[0]
save_best_mac_ex_host(best)
return cls(best, port, timeout, auto_reconnect, heartbeat_interval)
@staticmethod
def ping_all(
hosts: list[str] | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 5.0,
) -> list[tuple[str, float]]:
return ping_ex_all(hosts or get_mac_ex_hosts(), port, timeout)
# ------------------------------------------------------------------ #
# 连接管理
# ------------------------------------------------------------------ #
async def connect(self) -> None:
await self._conn.connect()
await self._login()
self._start_heartbeat()
async def close(self) -> None:
await self._stop_heartbeat()
await self._conn.close()
async def __aenter__(self) -> "AsyncMacExClient":
await self.connect()
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
await self.close()
def _start_heartbeat(self) -> None:
if self._heartbeat_interval <= 0:
return
if self._heartbeat_task is not None:
self._heartbeat_task.cancel()
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
async def _stop_heartbeat(self) -> None:
if self._heartbeat_task:
self._heartbeat_task.cancel()
try:
await self._heartbeat_task
except asyncio.CancelledError:
pass
self._heartbeat_task = None
async def _heartbeat_loop(self) -> None:
while True:
try:
await asyncio.sleep(self._heartbeat_interval)
await self._execute(GetExInstrumentCountCmd())
except asyncio.CancelledError:
break
except Exception:
pass
async def _login(self) -> None:
"""执行 MAC EX 登录命令。"""
await self._conn.execute(MacExLoginCmd())
async def _execute(self, cmd: "BaseCommand[_T]") -> _T:
async with self._execute_lock:
try:
return await self._conn.execute(cmd)
except TdxConnectionError:
if not self._auto_reconnect:
raise
await self._conn.close()
self._conn = AsyncExTdxConnection(self._host, self._port, self._timeout, mac_ex_mode=True)
await self._conn.connect()
await self._login()
return await self._conn.execute(cmd)
# ------------------------------------------------------------------ #
# 商品列表
# ------------------------------------------------------------------ #
async def goods_count(self, market: int | None = None) -> int:
"""获取商品总数。market=None 时返回全市场总数,否则返回指定市场的数量。"""
if market is None:
return await self._execute(GetExInstrumentCountCmd())
offset = await self._find_market_offset(market)
if offset < 0:
return 0
total = await self._execute(GetExInstrumentCountCmd())
n = 0
page = 1000
pos = offset
while pos < total:
batch = await self._execute(GetExInstrumentInfoCmd(start=pos, count=page))
if not batch:
break
for item in batch:
if item.market == market:
n += 1
elif item.market > market:
return n
pos += page
return n
async def goods_list(self, market: int, start: int = 0, count: int = 600) -> pd.DataFrame:
"""获取扩展市场商品列表(期货合约/港股/美股等)。"""
offset = await self._find_market_offset(market)
if offset < 0:
return pd.DataFrame()
total = await self._execute(GetExInstrumentCountCmd())
page_size = 1000
collected: list = []
skipped = 0
pos = offset
while pos < total and len(collected) < count:
batch = await self._execute(GetExInstrumentInfoCmd(start=pos, count=page_size))
if not batch:
break
for item in batch:
if item.market == market:
if skipped < start:
skipped += 1
else:
collected.append(item)
if len(collected) >= count:
break
elif item.market > market:
break
else:
pos += page_size
continue
break
return _to_df(collected)
async def _find_market_offset(self, market: int) -> int:
"""二分查找定位指定市场在全局商品列表中的起始偏移。"""
total = await self._execute(GetExInstrumentCountCmd())
if total == 0:
return -1
lo, hi = 0, total
while lo < hi:
mid = (lo + hi) // 2
items = await self._execute(GetExInstrumentInfoCmd(start=mid, count=1))
if not items:
hi = mid
continue
m = items[0].market
if m < market:
lo = mid + 1
else:
hi = mid
return lo
# ------------------------------------------------------------------ #
# 行情
# ------------------------------------------------------------------ #
async def goods_quotes(
self,
stocks: list[tuple[int, str]],
fields: Any = None,
) -> pd.DataFrame:
cmd = SymbolQuotesCmd(stocks, fields)
result: list[MacQuoteField] = await self._execute(cmd)
return _quotes_to_df(result)
async def goods_quotes_list(
self,
market: int,
start: int = 0,
count: int = 100,
sort_type: SortType = SortType.CODE,
sort_order: SortOrder = SortOrder.NONE,
) -> pd.DataFrame:
page_size = min(count, 80)
items_df = await self.goods_list(market, start=start, count=page_size)
if items_df.empty:
return pd.DataFrame()
stocks: list[tuple[int, str]] = [(market, row["code"]) for _, row in items_df.iterrows()]
cmd = SymbolQuotesCmd(stocks)
result: list[MacQuoteField] = await self._execute(cmd)
return _quotes_to_df(result)
# ------------------------------------------------------------------ #
# K 线
# ------------------------------------------------------------------ #
async def goods_kline(
self,
market: int,
code: str,
period: Period = Period.DAILY,
start: int = 0,
count: int = 800,
adjust: Adjust = Adjust.NONE,
) -> pd.DataFrame:
cmd = SymbolBarCmd(
market=market,
code=code,
period=period,
start=start,
count=count,
fq=adjust,
)
result = await self._execute(cmd)
return _to_df(result)
# ------------------------------------------------------------------ #
# 分时
# ------------------------------------------------------------------ #
async def goods_tick_chart(
self,
market: int,
code: str,
query_date: date | None = None,
) -> pd.DataFrame:
cmd = SymbolTickChartCmd(market=market, code=code, query_date=query_date)
result = await self._execute(cmd)
return _to_df(result)
async def goods_chart_sampling(
self,
market: int,
code: str,
) -> pd.DataFrame:
cmd = ChartSamplingCmd(market=market, code=code)
prices: list[float] = await self._execute(cmd)
if not prices:
return pd.DataFrame()
return pd.DataFrame({"price": prices})
# ------------------------------------------------------------------ #
# 成交
# ------------------------------------------------------------------ #
async def goods_transaction(
self,
market: int,
code: str,
query_date: date | None = None,
start: int = 0,
count: int = 2000,
) -> pd.DataFrame:
cmd = SymbolTransactionCmd(
market=market,
code=code,
query_date=query_date,
start=start,
count=count,
)
result = await self._execute(cmd)
return _to_df(result)
+7 -28
View File
@@ -2,34 +2,10 @@
from dataclasses import dataclass, field
# 扩展行情服务器(端口 7727),来源: pytdx_backup/util/best_ip.py
KNOWN_EX_HOSTS: list[str] = [
"106.14.95.149",
"112.74.214.43",
"119.147.86.171",
"119.97.185.5",
"120.24.0.77",
"47.92.127.181",
"59.175.238.38",
"61.152.107.141",
"61.152.107.171",
"47.107.75.159",
"120.25.218.6",
"43.139.173.246",
"159.75.90.107",
"106.52.170.195",
"139.9.191.175",
"175.24.47.69",
"150.158.9.199",
"150.158.20.127",
"49.235.119.116",
"49.234.13.160",
"116.205.143.214",
"124.71.223.19",
"113.45.175.47",
"123.60.173.210",
"118.89.69.202",
]
from ..config import get_ex_hosts, get_mac_ex_hosts
# 模块级别名,供外部 `from easy_tdx.ex.models import KNOWN_EX_HOSTS` 使用。
KNOWN_EX_HOSTS = get_ex_hosts()
# 已知扩展行情市场代码
KNOWN_EX_MARKETS: dict[int, str] = {
@@ -46,6 +22,9 @@ KNOWN_EX_MARKETS: dict[int, str] = {
74: "外盘",
}
# MAC 协议扩展行情服务器(端口 7727)
MAC_EX_HOSTS: list[str] = get_mac_ex_hosts()
_DEFAULT_EX_PORT = 7727
+16 -23
View File
@@ -5,8 +5,8 @@ from types import TracebackType
from typing import TYPE_CHECKING, TypeVar
from ...codec.frame import HEADER_SIZE, decompress_body, parse_header
from ...config import get_best_ex_host, get_ex_hosts
from ...exceptions import TdxConnectionError
from ..commands.setup import EX_SETUP_CMD
from ..models import KNOWN_EX_HOSTS
if TYPE_CHECKING:
@@ -19,17 +19,27 @@ _DEFAULT_TIMEOUT = 15.0
class AsyncExTdxConnection:
"""扩展行情异步 TCP 连接(asyncio,端口 7727,单包握手)。"""
"""扩展行情异步 TCP 连接(asyncio,端口 7727,单包握手)。
Parameters
----------
mac_ex_mode : bool
为 True 时自动将 MAC 命令的 head_flag 从 0x1C 转为 0x01
以兼容 MAC EX 服务器(需要 head_flag=0x01)。
"""
def __init__(
self,
host: str = KNOWN_EX_HOSTS[0],
host: str | None = None,
port: int = _DEFAULT_EX_PORT,
timeout: float = _DEFAULT_TIMEOUT,
*,
mac_ex_mode: bool = False,
) -> None:
self.host = host
self.host = host if host is not None else get_best_ex_host()
self.port = port
self.timeout = timeout
self.mac_ex_mode = mac_ex_mode
self._reader: asyncio.StreamReader | None = None
self._writer: asyncio.StreamWriter | None = None
self._io_lock = asyncio.Lock()
@@ -49,6 +59,8 @@ class AsyncExTdxConnection:
if self._writer is None or self._reader is None:
raise TdxConnectionError("未连接,请先调用 connect()")
request = cmd.build_request()
if self.mac_ex_mode and len(request) > 0 and request[0] == 0x1C:
request = b"\x01" + request[1:]
try:
self._writer.write(request)
await asyncio.wait_for(self._writer.drain(), timeout=self.timeout)
@@ -75,11 +87,6 @@ class AsyncExTdxConnection:
raise TdxConnectionError(f"无法连接 {self.host}:{self.port}: {e}") from e
self._reader = reader
self._writer = writer
try:
await self._send_setup()
except Exception:
await self._close_unlocked()
raise
async def _close_unlocked(self) -> None:
if self._writer is not None:
@@ -103,20 +110,6 @@ class AsyncExTdxConnection:
) -> None:
await self.close()
async def _send_setup(self) -> None:
"""发送单条扩展行情握手命令并丢弃响应。"""
assert self._writer is not None
assert self._reader is not None
self._writer.write(EX_SETUP_CMD)
await asyncio.wait_for(self._writer.drain(), timeout=self.timeout)
try:
hdr_buf = await self._recv_exact(HEADER_SIZE)
hdr = parse_header(hdr_buf)
if hdr.zipsize > 0:
await self._recv_exact(hdr.zipsize)
except (OSError, asyncio.TimeoutError, asyncio.IncompleteReadError):
pass
async def _recv_exact(self, n: int) -> bytes:
assert self._reader is not None
return await asyncio.wait_for(
+37 -41
View File
@@ -1,13 +1,15 @@
"""扩展行情同步 TCP 连接(端口 7727)。"""
import socket
import threading
import time
from types import TracebackType
from typing import TYPE_CHECKING, TypeVar
from ...codec.frame import HEADER_SIZE, decompress_body, parse_header
from ...config import get_best_ex_host, get_ex_hosts
from ...exceptions import TdxConnectionError
from ..commands.setup import EX_SETUP_CMD
from ..commands.get_instrument_count import GetExInstrumentCountCmd
from ..models import KNOWN_EX_HOSTS
if TYPE_CHECKING:
@@ -24,13 +26,14 @@ def ping_ex_host(
port: int = _DEFAULT_EX_PORT,
timeout: float = 5.0,
) -> float | None:
"""测量扩展行情服务器延迟(秒)。失败返回 None"""
"""测量扩展行情服务器延迟(秒)。通过发送 get_instrument_count 验证可用性"""
t0 = time.monotonic()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect((host, port))
sock.sendall(EX_SETUP_CMD)
cmd = GetExInstrumentCountCmd()
sock.sendall(cmd.build_request())
hdr_buf = _recv_exact_sock(sock, HEADER_SIZE)
hdr = parse_header(hdr_buf)
if hdr.zipsize > 0:
@@ -54,7 +57,7 @@ def ping_ex_all(
import concurrent.futures
if hosts is None:
hosts = KNOWN_EX_HOSTS
hosts = get_ex_hosts()
results: list[tuple[str, float]] = []
with concurrent.futures.ThreadPoolExecutor(max_workers=len(hosts)) as pool:
futures = {pool.submit(ping_ex_host, h, port, timeout): h for h in hosts}
@@ -78,21 +81,32 @@ def _recv_exact_sock(sock: socket.socket, n: int) -> bytes:
class ExTdxConnection:
"""扩展行情同步 TCP 连接(端口 7727,单包握手)。"""
"""扩展行情同步 TCP 连接(端口 7727,单包握手)。
Parameters
----------
mac_ex_mode : bool
为 True 时自动将 MAC 命令的 head_flag 从 0x1C 转为 0x01
以兼容 MAC EX 服务器(需要 head_flag=0x01)。
"""
def __init__(
self,
host: str = KNOWN_EX_HOSTS[0],
host: str | None = None,
port: int = _DEFAULT_EX_PORT,
timeout: float = _DEFAULT_TIMEOUT,
*,
mac_ex_mode: bool = False,
) -> None:
self.host = host
self.host = host if host is not None else get_best_ex_host()
self.port = port
self.timeout = timeout
self.mac_ex_mode = mac_ex_mode
self._sock: socket.socket | None = None
self._lock = threading.Lock()
def connect(self) -> None:
"""建立 TCP 连接并完成扩展行情握手"""
"""建立 TCP 连接扩展行情服务器不需要握手命令"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
try:
@@ -101,15 +115,6 @@ class ExTdxConnection:
sock.close()
raise TdxConnectionError(f"无法连接 {self.host}:{self.port}: {e}") from e
self._sock = sock
try:
self._send_setup()
except Exception:
try:
sock.close()
except OSError:
pass
self._sock = None
raise
def close(self) -> None:
if self._sock is not None:
@@ -121,18 +126,21 @@ class ExTdxConnection:
def execute(self, cmd: "BaseCommand[T]") -> T:
"""执行一条命令:发送请求,接收并解压响应,返回解析结果。"""
if self._sock is None:
raise TdxConnectionError("未连接,请先调用 connect()")
request = cmd.build_request()
try:
self._sock.sendall(request)
header_buf = self._recv_exact(HEADER_SIZE)
header = parse_header(header_buf)
raw_body = self._recv_exact(header.zipsize)
except OSError as e:
raise TdxConnectionError(f"通信错误: {e}") from e
body = decompress_body(header, raw_body)
return cmd.parse_response(body)
with self._lock:
if self._sock is None:
raise TdxConnectionError("未连接,请先调用 connect()")
request = cmd.build_request()
if self.mac_ex_mode and len(request) > 0 and request[0] == 0x1C:
request = b"\x01" + request[1:]
try:
self._sock.sendall(request)
header_buf = self._recv_exact(HEADER_SIZE)
header = parse_header(header_buf)
raw_body = self._recv_exact(header.zipsize)
except OSError as e:
raise TdxConnectionError(f"通信错误: {e}") from e
body = decompress_body(header, raw_body)
return cmd.parse_response(body)
def __enter__(self) -> "ExTdxConnection":
self.connect()
@@ -146,18 +154,6 @@ class ExTdxConnection:
) -> None:
self.close()
def _send_setup(self) -> None:
"""发送单条扩展行情握手命令并丢弃响应。"""
assert self._sock is not None
self._sock.sendall(EX_SETUP_CMD)
try:
hdr_buf = self._recv_exact(HEADER_SIZE)
hdr = parse_header(hdr_buf)
if hdr.zipsize > 0:
self._recv_exact(hdr.zipsize)
except OSError:
pass
def _recv_exact(self, n: int) -> bytes:
assert self._sock is not None
return _recv_exact_sock(self._sock, n)
+1
View File
@@ -0,0 +1 @@
"""MAC 协议客户端(板块、竞价、复权K线等高级接口)。"""
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
"""MAC 协议命令。"""
from .board_list import BoardListCmd
from .board_members_quotes import BoardMembersQuotesCmd
from .kline_offset import KlineOffsetCmd
from .server_info import ServerInfoCmd
from .symbol_auction import SymbolAuctionCmd
from .symbol_bar import SymbolBarCmd
from .symbol_belong_board import SymbolBelongBoardCmd
from .symbol_capital_flow import SymbolCapitalFlowCmd
from .symbol_info import SymbolInfoCmd
from .symbol_quotes import SymbolQuotesCmd
from .symbol_tick_chart import SymbolTickChartCmd
from .symbol_transaction import SymbolTransactionCmd
from .tick_charts import TickChartsCmd
from .unusual import UnusualCmd
__all__ = [
"BoardListCmd",
"BoardMembersQuotesCmd",
"KlineOffsetCmd",
"ServerInfoCmd",
"SymbolAuctionCmd",
"SymbolBarCmd",
"SymbolBelongBoardCmd",
"SymbolCapitalFlowCmd",
"SymbolInfoCmd",
"SymbolQuotesCmd",
"SymbolTickChartCmd",
"SymbolTransactionCmd",
"TickChartsCmd",
"UnusualCmd",
]
+96
View File
@@ -0,0 +1,96 @@
"""板块列表查询(0x1231)。"""
import struct
from ..._binary import unpack_from
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..enums import BoardType
from ..models import BoardInfo
# 板板信息 + 领涨股信息,每组 160 字节
# fmt: H(2) + 6s(6) + 16s(16) + 44s(44) + f(4) + f(4) + f(4) = 80
# x2 for board + symbol = 160
_RECORD_FMT = "<H6s16s44sfffH6s16s44sfff"
_RECORD_SIZE = struct.calcsize(_RECORD_FMT) # 160
class BoardListCmd(BaseCommand[list[BoardInfo]]):
"""查询板块列表。
Parameters
----------
board_type : BoardType
板块类型(行业、概念、风格等)。
start : int
起始偏移量。
page_size : int
每页数量。
"""
def __init__(
self,
board_type: BoardType = BoardType.ALL,
start: int = 0,
page_size: int = 150,
) -> None:
self._board_type = board_type
self._start = start
self._page_size = page_size
def build_request(self) -> bytes:
# <HHBBHH8x: page_size, board_type, sort_col(0), sort_order(0), start, flag(1)
body = struct.pack(
"<HHBBHH8x",
self._page_size,
int(self._board_type),
0, # sort_column: 0 = rise_speed
0, # sort_order
self._start,
1, # flag
)
return build_mac_request(0x1231, body)
def parse_response(self, body: bytes) -> list[BoardInfo]:
count_all, total = unpack_from("<HH", body, 0, "board_list header")
# 服务器返回 count_all = 2 * actual_countboard_info + symbol_info 各一份)
count = count_all // 2
results: list[BoardInfo] = []
for i in range(count):
offset = 4 + i * _RECORD_SIZE
(
market,
code_raw,
_pad1,
name_raw,
price,
rise_speed,
pre_close,
symbol_market,
symbol_code_raw,
_pad2,
symbol_name_raw,
symbol_price,
symbol_rise_speed,
symbol_pre_close,
) = unpack_from(_RECORD_FMT, body, offset, f"board_list record[{i}]")
results.append(
BoardInfo(
market=market,
code=code_raw.decode("gbk", errors="replace").rstrip("\x00"),
name=name_raw.decode("gbk", errors="replace").rstrip("\x00"),
price=price,
rise_speed=rise_speed,
pre_close=pre_close,
symbol_market=symbol_market,
symbol_code=symbol_code_raw.decode("gbk", errors="replace").rstrip("\x00"),
symbol_name=symbol_name_raw.decode("gbk", errors="replace").rstrip("\x00"),
symbol_price=symbol_price,
symbol_rise_speed=symbol_rise_speed,
symbol_pre_close=symbol_pre_close,
)
)
return results
@@ -0,0 +1,114 @@
"""板块成分报价查询(0x122C)。
响应格式与 symbol_quotes (0x122B) 相同:20 字节位图 + 总数 + 行数 + N 条记录。
每条记录:market(2) + code(22) + name(44) + active_fields × 4 字节。
"""
import struct
from ..._binary import unpack_from
from ...codec.bitmap import Fields, PresetField, build_bitmap, get_active_fields
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..enums import FilterType, SortOrder, SortType
from ..models import MacQuoteField
class BoardMembersQuotesCmd(BaseCommand[list[MacQuoteField]]):
"""查询板块成分股报价。
Parameters
----------
board_code : int
板块代码(如 int("881001"))。
sort_type : SortType
排序字段。
start : int
起始偏移量。
page_size : int
每页数量。
sort_order : SortOrder
排序方向。
fields : Fields
请求的字段集合。
exclude_flags : list[FilterType] | None
排除条件列表(如排除科创板、创业板等)。
"""
def __init__(
self,
board_code: int,
sort_type: SortType = SortType.CHANGE_PCT,
start: int = 0,
page_size: int = 80,
sort_order: SortOrder = SortOrder.NONE,
fields: Fields = PresetField.NONE,
exclude_flags: list[FilterType] | None = None,
) -> None:
self._board_code = board_code
self._sort_type = sort_type
self._start = start
self._page_size = page_size
self._sort_order = sort_order
self._fields = fields
self._exclude_flags = exclude_flags or []
def build_request(self) -> bytes:
# I:board_code, 9x padding, H:sort_type, I:start, H:page_size, B:sort_order, B:pad
body = struct.pack(
"<I9xHIHBB",
self._board_code,
int(self._sort_type),
self._start,
self._page_size,
int(self._sort_order),
0,
)
# 16 字节字段位图
bitmap = build_bitmap(self._fields)
body += bytes(bitmap[:16])
# 4 字节控制区: byte0=盘口, byte1=排除位, byte2=日内, byte3=控制(CTRL_EXTENDED=1)
b1 = sum(f.value for f in self._exclude_flags)
body += struct.pack("<BBBB", 0, b1, 0, 1)
return build_mac_request(0x122C, body)
def parse_response(self, body: bytes) -> list[MacQuoteField]:
# 响应位图(20 字节)
resp_bitmap = body[:20]
total, row_count = unpack_from("<IH", body, 20, "board_members header")
active_fields = get_active_fields(resp_bitmap[:16])
field_count = len(active_fields)
# 每行: market(2) + code(22) + name(44) = 68 + field_count * 4
row_len = 68 + field_count * 4
results: list[MacQuoteField] = []
for i in range(row_count):
row_start = 26 + i * row_len
market_raw = unpack_from("<H", body, row_start, f"board_members row[{i}] market")[0]
code_raw = body[row_start + 2 : row_start + 24]
name_raw = body[row_start + 24 : row_start + 68]
fields_dict: dict[str, object] = {}
for idx, (field_bit, fmt) in enumerate(active_fields):
val_bytes = body[row_start + 68 + idx * 4 : row_start + 68 + (idx + 1) * 4]
if len(val_bytes) < 4:
break
(value,) = struct.unpack(fmt, val_bytes)
fields_dict[field_bit.field_name] = value
results.append(
MacQuoteField(
market=market_raw,
code=code_raw.decode("gbk", errors="replace").rstrip("\x00"),
name=name_raw.decode("gbk", errors="replace").rstrip("\x00"),
fields=fields_dict,
)
)
return results
@@ -0,0 +1,47 @@
"""分时缩略采样命令(0x254D)。"""
from __future__ import annotations
import struct
from ..._binary import require_bytes, unpack_from
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
_MSG_ID = 0x254D
_CODE_LEN = 22
_RESPONSE_HEADER_SIZE = 42 # H(2) + 22s(22) + 9*H(18) = 42
class ChartSamplingCmd(BaseCommand[list[float]]):
"""获取分时缩略采样价格点。
返回 240 个 float 价格值(每分钟一个采样点)。
Args:
market: 扩展市场代码(ExMarket 枚举值)。
code: 证券代码(GBK 编码)。
"""
def __init__(self, market: int, code: str) -> None:
self.market = market
self.code = code
def build_request(self) -> bytes:
raw_code = self.code.encode("gbk")
padded = (raw_code + b"\x00" * _CODE_LEN)[:_CODE_LEN]
body = struct.pack("<H22sHH9x", self.market, padded, 1, 20)
return build_mac_request(_MSG_ID, body)
def parse_response(self, body: bytes) -> list[float]:
if len(body) < _RESPONSE_HEADER_SIZE:
return []
require_bytes(body, 0, _RESPONSE_HEADER_SIZE, "ChartSamplingCmd header")
(count,) = unpack_from("<H", body, 40, "chart_sampling count")
prices: list[float] = []
for i in range(count):
pos = _RESPONSE_HEADER_SIZE + i * 4
require_bytes(body, pos, 4, f"ChartSamplingCmd price[{i}]")
(p,) = unpack_from("<f", body, pos, f"chart_sampling price[{i}]")
prices.append(p)
return prices
+90
View File
@@ -0,0 +1,90 @@
"""文件查询与下载命令(0x1215 / 0x1217)。"""
from __future__ import annotations
import struct
from dataclasses import dataclass
from ..._binary import require_bytes, unpack_from
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
_FILELIST_MSG_ID = 0x1215
_FILEDL_MSG_ID = 0x1217
_FILENAME_LEN = 70
_FILENAME_PAD = 30
@dataclass(frozen=True)
class FileMeta:
"""文件列表查询结果。"""
offset: int
size: int
flag: int
hash: str
class FileListCmd(BaseCommand[FileMeta]):
"""查询远程文件元信息(大小、哈希等)。
Args:
filename: 远程文件名(GBK 编码)。
offset: 文件偏移(默认 0)。
"""
def __init__(self, filename: str, offset: int = 0) -> None:
self.filename = filename
self.offset = offset
def build_request(self) -> bytes:
raw_name = self.filename.encode("gbk")
padded = (raw_name + b"\x00" * _FILENAME_LEN)[:_FILENAME_LEN]
body = struct.pack("<I", self.offset) + padded + b"\x00" * _FILENAME_PAD
return build_mac_request(_FILELIST_MSG_ID, body)
def parse_response(self, body: bytes) -> FileMeta:
require_bytes(body, 0, 4 + 4 + 1 + 32, "FileListCmd")
offset, size, flag = unpack_from("<IIb", body, 0, "FileListCmd meta")
raw_hash = body[9:41]
hash_str = raw_hash.decode("ascii", errors="replace").rstrip("\x00")
return FileMeta(offset=offset, size=size, flag=flag, hash=hash_str)
class FileDownloadCmd(BaseCommand[bytes]):
"""分段下载远程文件内容。
Args:
filename: 远程文件名(GBK 编码)。
index: 分段序号(1-based)。
offset: 字节偏移。
size: 请求块大小(默认 30000)。
"""
def __init__(
self,
filename: str,
index: int = 1,
offset: int = 0,
size: int = 30000,
) -> None:
self.filename = filename
self.index = index
self.offset = offset
self.size = size
def build_request(self) -> bytes:
raw_name = self.filename.encode("gbk")
padded = (raw_name + b"\x00" * _FILENAME_LEN)[:_FILENAME_LEN]
body = (
struct.pack("<III", self.index, self.offset, self.size)
+ padded
+ b"\x00" * _FILENAME_PAD
)
return build_mac_request(_FILEDL_MSG_ID, body)
def parse_response(self, body: bytes) -> bytes:
if len(body) < 8:
return b""
return body[8:]
+77
View File
@@ -0,0 +1,77 @@
"""扩展市场商品列表命令(0x2562)。"""
from __future__ import annotations
import struct
from dataclasses import dataclass
from ..._binary import require_bytes, unpack_from
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
_MSG_ID = 0x2562
_MAX_COUNT = 1000
_RECORD_SIZE = 48
_RECORD_FMT = "<H23sHIBfffHH"
@dataclass(frozen=True)
class GoodsItem:
"""扩展市场商品信息。"""
name: str
category: int
u: int
index: int
switch: int
code: list[float]
c1: int
c2: int
class GoodsListCmd(BaseCommand[list[GoodsItem]]):
"""获取扩展市场(期货/期权等)商品列表。
Args:
market: 扩展市场代码(ExMarket 枚举值)。
start: 起始偏移(默认 0)。
count: 请求数量(最大 1000,默认 600)。
"""
def __init__(self, market: int, start: int = 0, count: int = 600) -> None:
if count > _MAX_COUNT:
raise ValueError(f"count 不能超过 {_MAX_COUNT},当前: {count}")
self.market = market
self.start = start
self.count = count
self.total: int = 0
def build_request(self) -> bytes:
body = struct.pack("<HII", self.market, self.start, self.count)
return build_mac_request(_MSG_ID, body)
def parse_response(self, body: bytes) -> list[GoodsItem]:
require_bytes(body, 0, 2, "GoodsListCmd header")
(total,) = unpack_from("<H", body, 0, "GoodsListCmd total")
self.total = total
items: list[GoodsItem] = []
for i in range(total):
offset = 2 + i * _RECORD_SIZE
require_bytes(body, offset, _RECORD_SIZE, f"GoodsListCmd record[{i}]")
category, raw_name, u, index, switch, v1, v2, v3, c1, c2 = unpack_from(
_RECORD_FMT, body, offset, f"GoodsListCmd record[{i}]",
)
name = raw_name.decode("gbk", errors="replace").rstrip("\x00")
items.append(
GoodsItem(
name=name,
category=category,
u=u,
index=index,
switch=switch,
code=[v1, v2, v3],
c1=c1,
c2=c2,
)
)
return items
+38
View File
@@ -0,0 +1,38 @@
"""K线偏移查询(0x124A)。"""
import struct
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..models import KlineOffsetInfo
class KlineOffsetCmd(BaseCommand[KlineOffsetInfo]):
"""查询K线数据偏移。
Parameters
----------
offset : int
偏移量(必须为 0)。
count : int
请求数量。
"""
def __init__(self, offset: int = 0, count: int = 128000) -> None:
self._offset = offset
self._count = count
def build_request(self) -> bytes:
# I:offset, I:count, 5 bytes padding
body = struct.pack("<II5x", self._offset, self._count)
return build_mac_request(0x124A, body)
def parse_response(self, body: bytes) -> KlineOffsetInfo:
if len(body) < 8:
return KlineOffsetInfo(total=0, returned=0)
# total 字段为大端序!
total = struct.unpack(">I", body[:4])[0]
returned = struct.unpack("<I", body[4:8])[0]
return KlineOffsetInfo(total=total, returned=returned)
+74
View File
@@ -0,0 +1,74 @@
"""服务器交易时段查询(0x120F)。"""
from ..._binary import unpack_from
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..models import ServerSession
class ServerInfoCmd(BaseCommand[ServerSession]):
"""查询服务器交易时段信息。"""
def build_request(self) -> bytes:
# 固定 68 字节请求体
header = bytes.fromhex("04002d31")
body = header + b"\x00" * 8 + b"\x00\x27\x06\x0e" + b"\x00" * 52
return build_mac_request(0x120F, body)
def parse_response(self, body: bytes) -> ServerSession:
if len(body) < 87:
return ServerSession(today="", last_trading_day="")
pos = 0
_count = unpack_from("<H", body, pos, "server_info count")[0]
pos += 2
# 8 bytes flags
pos += 8
# 3 bytes tag ("-1")
pos += 3
# 9 bytes reserved
pos += 9
def _parse_date(p: int) -> tuple[str, int]:
d = unpack_from("<I", body, p, "server_info date")[0]
return f"{d // 10000}-{d % 10000 // 100:02d}-{d % 100:02d}", p + 4
def _parse_session(p: int) -> tuple[list[dict[str, object]], int]:
vals = unpack_from("<8H", body, p, "server_info session")
sessions: list[dict[str, object]] = []
for i in range(0, 8, 2):
sessions.append(
{
"open": f"{vals[i] // 60}:{vals[i] % 60:02d}",
"close": f"{vals[i + 1] // 60}:{vals[i + 1] % 60:02d}",
}
)
return sessions, p + 16
today, pos = _parse_date(pos)
pos += 4 # ts1
sessions_1, pos = _parse_session(pos)
sessions_2, pos = _parse_session(pos)
pos += 1 # flag byte
last_trading_day, pos = _parse_date(pos)
pos += 4 # ts2
# Skip remaining fields
market_param_1 = 0
market_param_2 = 0
if pos + 8 <= len(body):
market_param_1 = unpack_from("<I", body, pos, "server_info param1")[0]
pos += 4
market_param_2 = unpack_from("<I", body, pos, "server_info param2")[0]
return ServerSession(
today=today,
last_trading_day=last_trading_day,
sessions_1=sessions_1,
sessions_2=sessions_2,
market_param_1=market_param_1,
market_param_2=market_param_2,
)
@@ -0,0 +1,66 @@
"""集合竞价数据查询(0x123D)。"""
import struct
from datetime import time
from ..._binary import unpack_from
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..models import AuctionItem
class SymbolAuctionCmd(BaseCommand[list[AuctionItem]]):
"""查询集合竞价数据。
Parameters
----------
market : int
市场代码。
code : str
证券代码。
start : int
起始偏移量。
count : int
请求数量。
"""
def __init__(self, market: int, code: str, start: int = 0, count: int = 500) -> None:
self._market = market
self._code = code
self._start = start
self._count = count
def build_request(self) -> bytes:
# H: market, 22s: code in GBK, I: start, I: count, 10 bytes padding
body = struct.pack(
"<H22sII10x",
self._market,
self._code.encode("gbk"),
self._start,
self._count,
)
return build_mac_request(0x123D, body)
def parse_response(self, body: bytes) -> list[AuctionItem]:
# 响应头: H:market, 22s:code, I:count, 8 bytes padding (zeros)
_market, _code, count = unpack_from("<H22sI", body, 0, "auction header")
items: list[AuctionItem] = []
for i in range(count):
offset = 36 + i * 16
if offset + 16 > len(body):
break
time_sec, price, matched, unmatched = unpack_from(
"<IfIi", body, offset, f"auction item[{i}]"
)
items.append(
AuctionItem(
time=time(time_sec // 3600, (time_sec % 3600) // 60, time_sec % 60),
price=price,
matched=matched,
unmatched=unmatched,
)
)
return items
+122
View File
@@ -0,0 +1,122 @@
"""MAC K 线数据命令(0x122E)。
获取单只股票的 K 线数据(支持复权)。
"""
import struct
from datetime import datetime
from ..._binary import unpack_from
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..enums import Adjust, Period
from ..models import MacBar
_MSG_ID = 0x122E
def _combine_datetime(ymd: int, time_num: int, is_intraday: bool) -> datetime:
"""将日期和可选时间组合为 datetime。
日线及以上周期 time_num 为 0,分时周期 time_num 含 HHMM 信息。
"""
year = ymd // 10000
month = (ymd % 10000) // 100
day = ymd % 100
if is_intraday and time_num:
hour = time_num // 3600
minute = (time_num % 3600) // 60
return datetime(year, month, day, hour, minute)
return datetime(year, month, day)
class SymbolBarCmd(BaseCommand[list[MacBar]]):
"""获取单只股票的 K 线数据。
Args:
market: 市场代码。
code: 6 位股票代码。
period: K 线周期。
times: 周期倍数(Period.MINS / Period.DAYS 时有效)。
start: 起始偏移(0 = 最新)。
count: 返回条数。
fq: 复权方式。
"""
def __init__(
self,
market: int,
code: str,
period: Period = Period.DAILY,
times: int = 1,
start: int = 0,
count: int = 700,
fq: Adjust = Adjust.NONE,
) -> None:
self._market = market
self._code = code
self._period = period
self._times = times
self._start = start
self._count = count
self._fq = fq
def build_request(self) -> bytes:
body = struct.pack(
"<H22sHH I HH bbb bH4s",
self._market,
self._code.encode("gbk"),
self._period,
self._times,
self._start,
self._count,
self._fq,
1,
1,
0,
1,
0,
b"",
)
return build_mac_request(_MSG_ID, body)
def parse_response(self, body: bytes) -> list[MacBar]:
# 头部: market(2) + code(22) + category(2) + flag(1) + count(2) + start(4) = 33
(category_flag, _flag, count, start) = unpack_from("<HBHI", body, 24, "symbol_bar header")
# 防止 count 异常导致越界读取
count = min(count, (len(body) - 33) // 36)
if count < 0:
count = 0
is_intraday = (
self._period < Period.DAILY
or self._period == Period.MIN_1
or self._period == Period.MINS
)
results: list[MacBar] = []
for i in range(count):
offset = 33 + i * 36
if offset + 36 > len(body):
break
(ymd, time_num, open_, high, low, close, amount, vol, float_shares) = unpack_from(
"<II7f", body, offset, f"symbol_bar bar[{i}]"
)
if ymd < 19900101 or ymd > 20991231:
continue
dt = _combine_datetime(ymd, time_num, is_intraday)
results.append(
MacBar(
datetime=dt,
open=open_,
high=high,
low=low,
close=close,
vol=vol,
amount=amount,
float_shares=float_shares,
)
)
return results
@@ -0,0 +1,90 @@
"""个股所属板块查询(0x1218 head=1)。"""
import json
import struct
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..models import BelongBoardInfo
# head=1 用于区分 symbol_belong_board 与 symbol_capital_flow (head=2)
_HEAD_FLAG = 1
def _to_float(value: object) -> float:
"""Safely convert JSON value to float."""
try:
return float(value) # type: ignore[arg-type]
except (ValueError, TypeError):
return 0.0
def _to_int(value: object) -> int:
"""Safely convert JSON value to int."""
try:
return int(float(value)) # type: ignore[arg-type]
except (ValueError, TypeError):
return 0
class SymbolBelongBoardCmd(BaseCommand[list[BelongBoardInfo]]):
"""查询个股所属板块。
Parameters
----------
market : int
市场代码。
code : str
证券代码。
"""
def __init__(self, market: int, code: str) -> None:
self._market = market
self._code = code
def build_request(self) -> bytes:
# H:market, 8s:code padded with spaces, 16s:padding, 21s:"Stock_GLHQ"
body = struct.pack(
"<H8s16x21s",
self._market,
self._code.encode("gbk"),
b"Stock_GLHQ",
)
return build_mac_request(0x1218, body, head_flag=_HEAD_FLAG)
def parse_response(self, body: bytes) -> list[BelongBoardInfo]:
# 响应头: H:market, 12s:query_info, 5x padding, 8s:ext = 27 bytes
if len(body) < 27:
return []
json_bytes = body[27:]
python_list: list[list[object]] = json.loads(json_bytes.decode("gbk", errors="replace"))
results: list[BelongBoardInfo] = []
if not python_list:
return results
for row in python_list:
n = len(row)
if n not in (9, 13):
continue
bt = _to_int(row[0])
mkt = _to_int(row[1])
board_code = str(row[2])
board_name = str(row[3])
close = _to_float(row[4]) if n > 4 and row[4] else 0.0
pre_close = _to_float(row[5]) if n > 5 and row[5] else 0.0
results.append(
BelongBoardInfo(
board_type=bt,
market=mkt,
board_code=board_code,
board_name=board_name,
close=close,
pre_close=pre_close,
)
)
return results
@@ -0,0 +1,85 @@
"""个股资金流向查询(0x1218 head=2)。"""
import json
import struct
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..models import CapitalFlowData
# head=2 用于区分 symbol_capital_flow 与 symbol_belong_board (head=1)
_HEAD_FLAG = 2
def _to_float(value: object) -> float:
"""Safely convert JSON value to float."""
try:
return float(value) # type: ignore[arg-type]
except (ValueError, TypeError):
return 0.0
class SymbolCapitalFlowCmd(BaseCommand[CapitalFlowData | None]):
"""查询个股资金流向。
Parameters
----------
market : int
市场代码。
code : str
证券代码。
"""
def __init__(self, market: int, code: str) -> None:
self._market = market
self._code = code
def build_request(self) -> bytes:
# H:market, 8s:code padded with spaces, 16s:padding, 21s:"Stock_ZJLX"
body = struct.pack(
"<H8s16x21s",
self._market,
self._code.encode("gbk"),
b"Stock_ZJLX",
)
return build_mac_request(0x1218, body, head_flag=_HEAD_FLAG)
def parse_response(self, body: bytes) -> CapitalFlowData | None:
# 响应头: H:market, 12s:query_info, 5x padding, 8s:ext = 27 bytes
if len(body) < 27:
return None
json_bytes = body[27:]
python_list: list[list[object]] = json.loads(json_bytes.decode("gbk"))
if len(python_list) < 2:
return None
today_data = python_list[0]
five_days_data = python_list[1]
# today_data: [main_in, main_out, retail_in, retail_out]
# five_days_data: [buy_5d, sell_5d, super_large, large, mid, small]
main_in = _to_float(today_data[0]) if len(today_data) > 0 else 0.0
main_out = _to_float(today_data[1]) if len(today_data) > 1 else 0.0
retail_in = _to_float(today_data[2]) if len(today_data) > 2 else 0.0
retail_out = _to_float(today_data[3]) if len(today_data) > 3 else 0.0
mid_net_5d = _to_float(five_days_data[4]) if len(five_days_data) > 4 else 0.0
large_net_5d = _to_float(five_days_data[3]) if len(five_days_data) > 3 else 0.0
return CapitalFlowData(
date="",
main_in=main_in,
main_out=main_out,
main_net=main_in - main_out,
small_in=retail_in,
small_out=retail_out,
small_net=retail_in - retail_out,
mid_in=0.0,
mid_out=0.0,
mid_net=mid_net_5d,
large_in=0.0,
large_out=0.0,
large_net=large_net_5d,
)
+88
View File
@@ -0,0 +1,88 @@
"""MAC 个股简要特征命令(0x122A)。
获取单只股票的实时快照信息。
"""
import struct
from datetime import datetime
from ..._binary import unpack_from
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..models import MacSymbolInfo
_MSG_ID = 0x122A
class SymbolInfoCmd(BaseCommand[MacSymbolInfo]):
"""获取个股简要特征。
Args:
market: 市场代码。
code: 6 位股票代码。
"""
def __init__(self, market: int, code: str) -> None:
self._market = market
self._code = code
def build_request(self) -> bytes:
body = struct.pack("<H22sI12x", self._market, self._code.encode("gbk"), 1)
return build_mac_request(_MSG_ID, body)
def parse_response(self, body: bytes) -> MacSymbolInfo:
# data[0:8] padding (zeros)
# data[8:74] market(2) + code(22) + name(44)
(market, code_raw, name_raw) = unpack_from("<H22s44s", body, 8, "symbol_info identity")
# data[76:96] padding (zeros)
# data[96:..] core fields
(
date_raw,
time_raw,
activity,
pre_close,
open,
high,
low,
close,
momentum,
vol,
amount,
inside_volume,
outside_volume,
) = unpack_from("<III5ffIfII", body, 96, "symbol_info core")
# data[148:..]
(_decimal, _a, _b, _c, _vr, turnover, avg) = unpack_from(
"<HIf20xI3f", body, 148, "symbol_info extra"
)
dt = datetime(
date_raw // 10000,
(date_raw % 10000) // 100,
date_raw % 100,
time_raw // 10000,
(time_raw % 10000) // 100,
time_raw % 100,
)
return MacSymbolInfo(
market=market,
code=code_raw.decode("gbk", errors="ignore").replace("\x00", ""),
name=name_raw.decode("gbk", errors="ignore").replace("\x00", ""),
time=dt,
activity=activity,
pre_close=pre_close,
open=open,
high=high,
low=low,
close=close,
momentum=momentum,
vol=int(vol),
amount=amount,
inside_volume=inside_volume,
outside_volume=outside_volume,
turnover=turnover,
avg=avg,
)
@@ -0,0 +1,99 @@
"""MAC 批量报价命令(0x122B)。
根据字段位图请求多只股票的自定义字段报价。
"""
from __future__ import annotations
import struct
from typing import Any
from ..._binary import unpack_from
from ...codec.bitmap import (
FIELD_POSTPROCESS,
Fields,
build_bitmap,
get_active_fields,
)
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..models import MacQuoteField
_MSG_ID = 0x122B
class SymbolQuotesCmd(BaseCommand[list[MacQuoteField]]):
"""批量获取自定义字段报价。
Args:
stocks: [(market, code), ...] 列表。
fields: 字段选择,默认 PresetField.COMMON。
"""
def __init__(self, stocks: list[tuple[int, str]], fields: Fields | None = None) -> None:
if not stocks:
raise ValueError("stocks 不能为空")
self._stocks = stocks
# 默认不请求任何字段时使用 COMMON 需要导入 PresetField
# 这里延迟导入避免循环。
if fields is None:
from ...codec.bitmap import PresetField
fields = PresetField.COMMON
self._fields = fields
self._bitmap = bytes(build_bitmap(fields))
def build_request(self) -> bytes:
body = bytearray(self._bitmap)
body += struct.pack("<H", len(self._stocks))
for market, code in self._stocks:
body += struct.pack("<H22s", market, code.encode("gbk"))
return build_mac_request(_MSG_ID, bytes(body))
def parse_response(self, body: bytes) -> list[MacQuoteField]:
pos = 0
field_bitmap = body[pos : pos + 20]
pos += 20
(total_stocks, row_count) = unpack_from("<IH", body, pos, "symbol_quotes header")
pos += 6
active = get_active_fields(field_bitmap[:16])
field_count = len(active)
row_len = 68 + 4 * field_count
results: list[MacQuoteField] = []
for _ in range(row_count):
row_end = pos + row_len
if row_end > len(body):
break
row_data = body[pos:row_end]
pos = row_end
(market, code_raw, name_raw) = unpack_from("<H22s44s", row_data, 0, "symbol_quotes row")
code = code_raw.decode("gbk", errors="ignore").replace("\x00", "")
name = name_raw.decode("gbk", errors="ignore").replace("\x00", "")
fields_dict: dict[str, Any] = {}
if field_count:
for idx, (field_bit, fmt) in enumerate(active):
value_bytes = row_data[68 + idx * 4 : 68 + (idx + 1) * 4]
(value,) = struct.unpack(fmt, value_bytes)
# 后处理钩子
post_fn = FIELD_POSTPROCESS.get(field_bit.value)
if post_fn is not None:
value = post_fn(value, market) # type: ignore[operator]
fields_dict[field_bit.field_name] = value
results.append(
MacQuoteField(
market=market,
code=code,
name=name,
fields=fields_dict,
)
)
return results
@@ -0,0 +1,112 @@
"""MAC 单日分时图命令(0x122D)。
获取单只股票某日的分时数据。
"""
import struct
from datetime import date, time
from ..._binary import unpack_from
from ...codec.mac_frame import build_mac_request
from ...commands.base import BaseCommand
from ..models import MacTick, MacTickChart
_MSG_ID = 0x122D
class SymbolTickChartCmd(BaseCommand[MacTickChart]):
"""获取单日分时图。
Args:
market: 市场代码。
code: 6 位股票代码。
query_date: 查询日期(None 或 date(0,0,0) 表示今天)。
"""
def __init__(
self,
market: int,
code: str,
query_date: date | None = None,
) -> None:
self._market = market
self._code = code
if query_date is not None:
self._ymd = query_date.year * 10000 + query_date.month * 100 + query_date.day
else:
self._ymd = 0
def build_request(self) -> bytes:
body = struct.pack(
"<H22sI5H",
self._market,
self._code.encode("gbk"),
self._ymd,
1,
0,
0,
0,
0,
)
return build_mac_request(_MSG_ID, body)
def parse_response(self, body: bytes) -> MacTickChart:
# 头部: market(2) + code(22) + query_date(4) + reserved(1) + ref_price(4) + count(2)
(market, code_raw, query_date, reserved, ref_price, count) = unpack_from(
"<H22sIBfH", body, 0, "tick_chart header"
)
ticks: list[MacTick] = []
for i in range(count):
offset = 35 + i * 18
(minutes, price, avg, vol, momentum) = unpack_from(
"<HffIf", body, offset, f"tick_chart tick[{i}]"
)
ticks.append(
MacTick(
time=time(minutes // 60 % 24, minutes % 60),
price=price,
avg=avg,
vol=vol,
momentum=momentum,
)
)
# 尾部元数据
tail_offset = 35 + count * 18
(
name_raw,
_decimal,
_category,
_vol_unit,
_date_raw,
_time_raw,
pre_close,
open,
high,
low,
close,
_momentum_tail,
vol,
amount,
_tail_pad2,
turnover,
avg_tail,
_industry,
) = unpack_from("<44sBHf5x2I5ffIf12s2fI", body, tail_offset, "tick_chart tail")
return MacTickChart(
market=market,
code=code_raw.decode("gbk", errors="ignore").replace("\x00", ""),
name=name_raw.decode("gbk", errors="ignore").replace("\x00", ""),
pre_close=pre_close,
open=open,
high=high,
low=low,
close=close,
vol=int(vol),
amount=amount,
turnover=turnover,
avg=avg_tail,
charts=ticks,
)

Some files were not shown because too many files have changed in this diff Show More