mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 13:24:18 +08:00
feat: v1.1.0 - MAC protocol, CLI tool, extended markets, unified client
- Add MacClient/AsyncMacClient with full MAC protocol support (quotes, kline with adjustment, tick charts, transactions, boards, capital flow, auction, unusual, symbol info, server info) - Add MacExClient/AsyncMacExClient for extended markets (HK, US, futures) - Add UnifiedTdxClient auto-routing between A-share and extended markets - Add `easy-tdx` CLI tool with JSON default output, Agent-friendly - Add field bitmap protocol for custom quote field selection - Fix quote-list missing fields (default to BASIC+VOLUME preset) - Add config.py with centralized host management and auto-discovery - Add 50+ examples covering all APIs (01-20) - Rewrite README with CLI-first, Agent-friendly documentation - Bump version to 1.1.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
67a0415c38
commit
4820b4a049
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 集合竞价
|
||||
|
||||
@@ -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 集合竞价
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
"""演示:获取个股当日资金流向(基于 L1 逐笔数据统计)。
|
||||
|
||||
资金分为四级: 超大(>100万)、大(20-100万)、中(4-20万)、小(<4万)。
|
||||
使用 TdxClient 标准协议客户端,调用 get_fund_flow() 获取个股当日资金流向分布。
|
||||
返回单行 DataFrame(FundFlow 模型),包含四级资金的流入/流出金额。
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 只
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 日期 YYYYMMDD(int 格式)
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 字段(单行 DataFrame,sessions 为嵌套结构)。
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,65 @@
|
||||
"""演示:扩展市场商品列表(港股主板)。
|
||||
|
||||
使用 MacExClient(MAC 协议扩展市场客户端,端口 7727)获取港股主板的商品列表和总数。
|
||||
goods_list 返回 DataFrame,goods_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
|
||||
@@ -0,0 +1,72 @@
|
||||
"""演示:扩展市场 K 线数据(港股/美股/期货)。
|
||||
|
||||
使用 MacExClient(MAC 协议扩展市场客户端,端口 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
|
||||
@@ -0,0 +1,42 @@
|
||||
"""演示:扩展市场实时报价(港股/美股)。
|
||||
|
||||
使用 MacExClient(MAC 协议扩展市场客户端,端口 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
|
||||
@@ -0,0 +1,61 @@
|
||||
"""演示:扩展市场分时图数据(港股)。
|
||||
|
||||
使用 MacExClient(MAC 协议扩展市场客户端,端口 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
|
||||
@@ -0,0 +1,58 @@
|
||||
"""演示:UnifiedTdxClient 统一入口,同一连接内访问 A 股和扩展市场。
|
||||
|
||||
UnifiedTdxClient 内部自动管理两个客户端:
|
||||
- MacClient(A 股,端口 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
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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字节) — zipsize(body 长度)
|
||||
偏移 8: H (2字节) — unzipsize(同 zipsize,MAC 不压缩请求)
|
||||
|
||||
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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,715 @@
|
||||
"""MAC 协议扩展市场高层 API:MacExClient(同步)和 AsyncMacExClient(asyncio)。
|
||||
|
||||
期货/港股/美股等扩展市场通过 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)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""MAC 协议客户端(板块、竞价、复权K线等高级接口)。"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
]
|
||||
@@ -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_count(board_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
|
||||
@@ -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:]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
"""MAC 逐笔成交命令(0x122F)。
|
||||
|
||||
获取单只股票的逐笔成交数据。
|
||||
"""
|
||||
|
||||
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 MacTransaction
|
||||
|
||||
_MSG_ID = 0x122F
|
||||
|
||||
|
||||
class SymbolTransactionCmd(BaseCommand[list[MacTransaction]]):
|
||||
"""获取逐笔成交数据。
|
||||
|
||||
Args:
|
||||
market: 市场代码。
|
||||
code: 6 位股票代码。
|
||||
query_date: 查询日期(None 表示今天)。
|
||||
start: 起始偏移。
|
||||
count: 返回条数。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
query_date: date | None = None,
|
||||
start: int = 0,
|
||||
count: int = 1000,
|
||||
) -> 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
|
||||
self._start = start
|
||||
self._count = count
|
||||
|
||||
def build_request(self) -> bytes:
|
||||
body = struct.pack(
|
||||
"<H22sIIH10x",
|
||||
self._market,
|
||||
self._code.encode("gbk"),
|
||||
self._ymd,
|
||||
self._start,
|
||||
self._count,
|
||||
)
|
||||
return build_mac_request(_MSG_ID, body)
|
||||
|
||||
def parse_response(self, body: bytes) -> list[MacTransaction]:
|
||||
# 头部: market(2) + code(22) + query_date(4) + flag(1) + count(2) + start(4) + total(4) = 39
|
||||
(count,) = unpack_from("<H", body, 29, "transaction count")
|
||||
|
||||
results: list[MacTransaction] = []
|
||||
for i in range(count):
|
||||
offset = 39 + i * 18
|
||||
(time_sec, price, volume, trade_count, bs_flag) = unpack_from(
|
||||
"<IfIIH", body, offset, f"transaction item[{i}]"
|
||||
)
|
||||
results.append(
|
||||
MacTransaction(
|
||||
time=time(time_sec // 3600, time_sec % 3600 // 60, time_sec % 60),
|
||||
price=price,
|
||||
vol=volume,
|
||||
trade_count=trade_count,
|
||||
bs_flag=bs_flag,
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user