From 00825eb24ab68b92e3e35355b0c93040953acea9 Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Fri, 22 May 2026 04:19:07 +0800 Subject: [PATCH 1/5] feat: merge datetime fields in DataFrame output, hide MinuteBar internal fields - K-line: daily+ periods output 'date' only, minute periods output 'datetime' - Transactions (tick-by-tick): combine date param + hour/minute into 'datetime' - XdxrRecord, HistoricalFundFlow: year/month/day merged to 'date' - MinuteBar: rename unknown_1 to _unknown_1 (hidden from DataFrame) - MinuteBar: add datetime column computed from bar index (A-share 240-bar pattern) - get_minute_time_data: use history endpoint only (current-day endpoint broken in pytdx too) - Update all examples to reflect new DataFrame column names --- .claude/settings.local.json | 4 +- CLAUDE.md | 3 +- examples/01_connection/async_connect.py | 12 +- examples/02_market_info/market_stat.py | 14 +- examples/02_market_info/security_list.py | 79 ++-- examples/02_market_info/security_list_all.py | 74 ++-- examples/02_market_info/security_quotes.py | 23 +- examples/03_kline/index_bars.py | 17 +- examples/03_kline/security_bars.py | 16 +- examples/04_minute/history_minute_data.py | 12 +- examples/04_minute/minute_time_data.py | 12 +- .../05_transaction/history_transaction.py | 14 +- examples/05_transaction/transaction_data.py | 14 +- examples/06_finance/company_info.py | 40 +- examples/06_finance/finance_info.py | 17 +- examples/06_finance/price_limits.py | 23 +- examples/06_finance/xdxr_info.py | 13 +- examples/07_block/block_info.py | 11 +- examples/08_fund_flow/fund_flow.py | 18 +- examples/08_fund_flow/history_fund_flow.py | 11 +- examples/09_file_download/report_file.py | 25 +- examples/10_offline/block_data.py | 11 +- pyproject.toml | 3 +- src/easy_tdx/_df.py | 111 +++++ src/easy_tdx/client.py | 379 +++++++++--------- src/easy_tdx/commands/minute_time.py | 2 +- src/easy_tdx/models/timeseries.py | 6 +- tests/fixtures/minute_time.json | 2 +- tests/unit/test_a_share_extensions.py | 341 ++++++++-------- tests/unit/test_block_info.py | 32 +- tests/unit/test_commands_offline.py | 44 +- 31 files changed, 720 insertions(+), 663 deletions(-) create mode 100644 src/easy_tdx/_df.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bbe7f0b..35e19d1 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -15,7 +15,9 @@ "Bash(python3 *)", "Bash(where mypy *)", "Bash(pip list *)", - "Bash(uv run *)" + "Bash(uv run *)", + "Bash(git stash *)", + "Bash(dir /s /b src\\\\xmtdx)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index 2818a58..ab9c729 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,4 +51,5 @@ commands 层不依赖 transport,可独立单测。修改 codec 或 commands - ruff: line-length 100, target py310, rules: E/F/I/UP - mypy strict mode -- 纯标准库,零运行时依赖。新增代码不要引入第三方库。 +- 所有 `get_*` 公开方法返回 `pd.DataFrame`(通过 `_df._to_df()` 转换)。内部方法仍使用 dataclass 列表。 +- 依赖:pandas(>=2.0)、tzdata(>=2024.1)。 diff --git a/examples/01_connection/async_connect.py b/examples/01_connection/async_connect.py index a69f4c8..0f6a0cf 100644 --- a/examples/01_connection/async_connect.py +++ b/examples/01_connection/async_connect.py @@ -1,7 +1,8 @@ """演示:异步客户端连接与基本用法。""" import asyncio -from easy_tdx import AsyncTdxClient, Market, KlineCategory + +from easy_tdx import AsyncTdxClient, KlineCategory, Market async def main(): @@ -12,13 +13,8 @@ async def main(): # 自动优选服务器 async with AsyncTdxClient.from_best_host() as c: - bars = await c.get_security_bars( - Market.SH, "600000", KlineCategory.DAY, 0, 5 - ) - for bar in bars: - print(f"{bar.year}-{bar.month:02d}-{bar.day:02d} " - f"开:{bar.open:.2f} 高:{bar.high:.2f} " - f"低:{bar.low:.2f} 收:{bar.close:.2f}") + df = await c.get_security_bars(Market.SH, "600000", KlineCategory.DAY, 0, 5) + print(df.to_string(index=False)) asyncio.run(main()) diff --git a/examples/02_market_info/market_stat.py b/examples/02_market_info/market_stat.py index 0f805df..22f9110 100644 --- a/examples/02_market_info/market_stat.py +++ b/examples/02_market_info/market_stat.py @@ -1,19 +1,7 @@ """演示:获取全市场涨跌统计概况。""" -import pandas as pd from easy_tdx import TdxClient with TdxClient.from_best_host() as c: stat = c.get_market_stat() - df = pd.DataFrame([{ - "上涨": stat.up_count, - "下跌": stat.down_count, - "平盘": stat.neutral_count, - "停牌(估算)": stat.suspended_count, - "总计": stat.total_count, - "成交额(亿)": round(stat.total_amount / 1e8, 2), - "总市值(万亿)": round(stat.total_market_cap / 1e12, 4), - "涨停": stat.limit_up_count, - "跌停": stat.limit_down_count, - }]) - print(df.T.to_string(header=False)) + print(stat) diff --git a/examples/02_market_info/security_list.py b/examples/02_market_info/security_list.py index 7ea135f..6660110 100644 --- a/examples/02_market_info/security_list.py +++ b/examples/02_market_info/security_list.py @@ -1,40 +1,63 @@ -"""演示:获取市场证券列表(分页)。 - -展示 SecurityInfo 全部字段的中文映射与表结构。 -""" +"""演示:获取市场证券列表(分页)。""" import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: - stocks = c.get_security_list(Market.SH, start=0) + df = c.get_security_list(Market.SH, start=0) # 表结构说明 print("=" * 70) print("SecurityInfo 表结构(字段中英文对照)") print("=" * 70) - schema = pd.DataFrame([ - {"英文字段": "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()"}, - ]) + schema = pd.DataFrame( + [ + { + "英文字段": "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()", + }, + ] + ) print(schema.to_string(index=False)) - # 全字段中文 DataFrame - print(f"\n沪市第 1 页,共 {len(stocks)} 只:") - df = pd.DataFrame([{ - "市场": s.market.name, - "代码": s.code, - "名称": s.name, - "成交量单位(股/手)": s.volunit, - "价格小数位": s.decimal_point, - "昨收价": s.pre_close, - "通达信行业": s.industry_tdx or "", - "申万行业": s.industry_sw or "", - } for s in stocks]) + print(f"\n沪市第 1 页,共 {len(df)} 只:") print(df.head(20).to_string(index=False)) diff --git a/examples/02_market_info/security_list_all.py b/examples/02_market_info/security_list_all.py index 5a35120..408179a 100644 --- a/examples/02_market_info/security_list_all.py +++ b/examples/02_market_info/security_list_all.py @@ -1,10 +1,10 @@ """演示:获取沪深 A 股完整列表(含行业映射)。 -展示 SecurityInfo 全部字段(含扩展行业字段)的中文映射。 注意:此方法需要拉取 tdxhy.cfg 并遍历全部证券,耗时较长。 """ import logging + import pandas as pd from easy_tdx import TdxClient @@ -13,34 +13,60 @@ logging.basicConfig(level=logging.INFO, format="%(message)s") # timeout 调大到 30 秒,避免全量拉取时分页请求超时 with TdxClient.from_best_host(timeout=30.0) as c: - all_stocks = c.get_security_list_all() + df = c.get_security_list_all() # 表结构说明 print("=" * 70) print("SecurityInfo 表结构(字段中英文对照)") print("=" * 70) - schema = pd.DataFrame([ - {"英文字段": "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", "说明": "如 T1001,来自 tdxhy.cfg"}, - {"英文字段": "industry_sw", "中文含义": "申万行业", "类型": "str", "说明": "如 X500102,来自 tdxhy.cfg"}, - ]) + schema = pd.DataFrame( + [ + { + "英文字段": "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", + "说明": "如 T1001,来自 tdxhy.cfg", + }, + { + "英文字段": "industry_sw", + "中文含义": "申万行业", + "类型": "str", + "说明": "如 X500102,来自 tdxhy.cfg", + }, + ] + ) print(schema.to_string(index=False)) - # 全字段中文 DataFrame - print(f"\n沪深 A 股总数: {len(all_stocks)}") - df = pd.DataFrame([{ - "市场": s.market.name, - "代码": s.code, - "名称": s.name, - "成交量单位(股/手)": s.volunit, - "价格小数位": s.decimal_point, - "昨收价": s.pre_close, - "通达信行业": s.industry_tdx or "", - "申万行业": s.industry_sw or "", - } for s in all_stocks]) + print(f"\n沪深 A 股总数: {len(df)}") print(df.head(20).to_string(index=False)) diff --git a/examples/02_market_info/security_quotes.py b/examples/02_market_info/security_quotes.py index 763be38..dda7333 100644 --- a/examples/02_market_info/security_quotes.py +++ b/examples/02_market_info/security_quotes.py @@ -1,7 +1,6 @@ """演示:批量获取实时五档行情。最多支持 80 只/次。""" -import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: stocks = [ @@ -10,16 +9,10 @@ with TdxClient.from_best_host() as c: (Market.SZ, "000001"), # 平安银行 (Market.SZ, "000858"), # 五粮液 ] - quotes = c.get_security_quotes(stocks) - df = pd.DataFrame([{ - "代码": q.code, - "现价": q.price, - "涨跌幅%": (q.price - q.pre_close) / q.pre_close * 100, - "今开": q.open, - "最高": q.high, - "最低": q.low, - "昨收": q.pre_close, - "成交量(手)": q.vol, - "成交额": q.amount, - } for q in quotes]) - print(df.to_string(index=False)) + df = c.get_security_quotes(stocks) + df["change_pct"] = (df["price"] - df["pre_close"]) / df["pre_close"] * 100 + print( + df[ + ["code", "price", "change_pct", "open", "high", "low", "pre_close", "vol", "amount"] + ].to_string(index=False) + ) diff --git a/examples/03_kline/index_bars.py b/examples/03_kline/index_bars.py index 1ee5b03..57284e7 100644 --- a/examples/03_kline/index_bars.py +++ b/examples/03_kline/index_bars.py @@ -6,20 +6,9 @@ 创业板指: Market.SZ, "399006" """ -import pandas as pd -from easy_tdx import TdxClient, Market, KlineCategory +from easy_tdx import KlineCategory, Market, TdxClient with TdxClient.from_best_host() as c: - bars = c.get_index_bars(Market.SH, "999999", KlineCategory.DAY, 0, 10) - df = pd.DataFrame([{ - "日期": f"{b.year}-{b.month:02d}-{b.day:02d}", - "开盘": b.open, - "最高": b.high, - "最低": b.low, - "收盘": b.close, - "成交量": b.vol, - "成交额": b.amount, - } for b in reversed(bars)]) + df = c.get_index_bars(Market.SH, "999999", KlineCategory.DAY, 0, 10) print("上证指数 日K线:") - fmt = {"成交量": lambda x: f"{x:,.0f}", "成交额": lambda x: f"{x:,.0f}"} - print(df.to_string(index=False, formatters=fmt)) + print(df.to_string(index=False)) diff --git a/examples/03_kline/security_bars.py b/examples/03_kline/security_bars.py index fa4d6e3..d52ca2b 100644 --- a/examples/03_kline/security_bars.py +++ b/examples/03_kline/security_bars.py @@ -5,19 +5,9 @@ K 线类别: KlineCategory.DAY / WEEK / MONTH / YEAR """ -import pandas as pd -from easy_tdx import TdxClient, Market, KlineCategory +from easy_tdx import KlineCategory, Market, TdxClient with TdxClient.from_best_host() as c: - bars = c.get_security_bars(Market.SZ, "002176", KlineCategory.DAY, 0, 100) - df = pd.DataFrame([{ - "日期": f"{b.year}-{b.month:02d}-{b.day:02d}", - "开盘": b.open, - "最高": b.high, - "最低": b.low, - "收盘": b.close, - "成交量": b.vol, - "成交额": b.amount, - } for b in reversed(bars)]) - print("上证指数 日K线:") + df = c.get_security_bars(Market.SZ, "002176", KlineCategory.DAY, 0, 100) + print("江特电机 日K线:") print(df.to_string(index=False)) diff --git a/examples/04_minute/history_minute_data.py b/examples/04_minute/history_minute_data.py index 0612f3f..ea83929 100644 --- a/examples/04_minute/history_minute_data.py +++ b/examples/04_minute/history_minute_data.py @@ -1,15 +1,9 @@ """演示:获取历史某日分时数据。date 参数为 YYYYMMDD 格式的整数。""" -import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: date = 20250110 - bars = c.get_history_minute_time_data(Market.SH, "600000", date) - df = pd.DataFrame([{ - "序号": i + 1, - "价格": bar.price, - "成交量": bar.vol, - } for i, bar in enumerate(bars)]) + df = c.get_history_minute_time_data(Market.SH, "600000", date) print(f"浦发银行 {date} 分时数据,共 {len(df)} 条:") - print(df.to_string(index=False)) + print(df.head(20).to_string(index=False)) diff --git a/examples/04_minute/minute_time_data.py b/examples/04_minute/minute_time_data.py index 61498f1..be5c2c6 100644 --- a/examples/04_minute/minute_time_data.py +++ b/examples/04_minute/minute_time_data.py @@ -1,14 +1,8 @@ """演示:获取今日分时数据(240 条)。""" -import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: - bars = c.get_minute_time_data(Market.SH, "600000") - df = pd.DataFrame([{ - "序号": i + 1, - "价格": bar.price, - "成交量": bar.vol, - } for i, bar in enumerate(bars)]) + df = c.get_minute_time_data(Market.SH, "600000") print(f"浦发银行今日分时,共 {len(df)} 条:") - print(df.to_string(index=False)) + print(df.head(20).to_string(index=False)) diff --git a/examples/05_transaction/history_transaction.py b/examples/05_transaction/history_transaction.py index e90a10f..8150dbc 100644 --- a/examples/05_transaction/history_transaction.py +++ b/examples/05_transaction/history_transaction.py @@ -1,16 +1,10 @@ """演示:获取历史逐笔成交数据。date 参数为 YYYYMMDD 格式的整数。""" -import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: date = 20250110 - records = c.get_history_transaction_data(Market.SH, "600000", date, 0, 20) - df = pd.DataFrame([{ - "时间": f"{r.hour:02d}:{r.minute:02d}", - "成交价": r.price, - "成交量": r.vol, - "方向": "买" if r.buyorsell == 0 else "卖", - } for r in records]) + df = c.get_history_transaction_data(Market.SH, "600000", date, 0, 20) + df["方向"] = df["buyorsell"].map({0: "买", 1: "卖", 2: "中性", 8: "集合竞价"}) print(f"浦发银行 {date} 最近 {len(df)} 笔成交:") - print(df.to_string(index=False)) + print(df[["datetime", "price", "vol", "方向"]].to_string(index=False)) diff --git a/examples/05_transaction/transaction_data.py b/examples/05_transaction/transaction_data.py index 45e3ce6..395f13e 100644 --- a/examples/05_transaction/transaction_data.py +++ b/examples/05_transaction/transaction_data.py @@ -1,15 +1,9 @@ """演示:获取当日逐笔成交数据。""" -import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: - records = c.get_transaction_data(Market.SH, "600000", 0, 20) - df = pd.DataFrame([{ - "时间": f"{r.hour:02d}:{r.minute:02d}", - "成交价": r.price, - "成交量": r.vol, - "方向": "买" if r.buyorsell == 0 else "卖", - } for r in records]) + df = c.get_transaction_data(Market.SH, "600000", 0, 20) + df["方向"] = df["buyorsell"].map({0: "买", 1: "卖", 2: "中性", 8: "集合竞价"}) print(f"浦发银行最近 {len(df)} 笔成交:") - print(df.to_string(index=False)) + print(df[["datetime", "price", "vol", "方向"]].to_string(index=False)) diff --git a/examples/06_finance/company_info.py b/examples/06_finance/company_info.py index c72d280..16b3ffe 100644 --- a/examples/06_finance/company_info.py +++ b/examples/06_finance/company_info.py @@ -1,7 +1,6 @@ """演示:获取公司信息目录与各个分类的详细内容。""" -import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient CODE = "600519" NAME = "贵州茅台" @@ -28,51 +27,36 @@ SHOW_CATEGORIES = [ ] -def show_categories(categories): - """显示公司信息目录。""" - df = pd.DataFrame([{ - "目录名": cat.name, - "文件名": cat.filename, - "起始偏移": cat.start, - "内容长度": cat.length, - } for cat in categories]) - print(f"{NAME} 公司信息目录:") - print(df.to_string(index=False)) - - def show_category_content(client, categories, category_name, max_chars=500): """获取并展示指定分类的内容。""" - cat = next((c for c in categories if c.name == category_name), None) - if not cat: + row = categories[categories["name"] == category_name] + if row.empty: print(f" 未找到分类: {category_name}") return + r = row.iloc[0] content = client.get_company_info_content( - MARKET, CODE, cat.filename, cat.start, cat.length + MARKET, CODE, r["filename"], int(r["start"]), int(r["length"]) ) text = content.strip() if len(text) > max_chars: text = text[:max_chars] + f"\n... (共 {len(content.strip())} 字,仅显示前 {max_chars} 字)" - print(f"\n{'='*60}") - print(f"【{cat.name}】 (共 {cat.length} 字节)") - print(f"{'='*60}") + print(f"\n{'=' * 60}") + print(f"【{r['name']}】 (共 {r['length']} 字节)") + print(f"{'=' * 60}") print(text) -def show_all_categories(client, categories): - """依次展示所有 SHOW_CATEGORIES 中列出的分类内容。""" - for name in SHOW_CATEGORIES: - show_category_content(client, categories, name) - - with TdxClient.from_best_host() as c: categories = c.get_company_info_category(MARKET, CODE) # 1. 显示目录 - show_categories(categories) + print(f"{NAME} 公司信息目录:") + print(categories.to_string(index=False)) # 2. 显示所有分类内容(每个分类默认只显示前500字) - show_all_categories(c, categories) + for name in SHOW_CATEGORIES: + show_category_content(c, categories, name) # 3. 也可以单独获取某个分类的完整内容,例如: # show_category_content(c, categories, "公司概况", max_chars=99999) diff --git a/examples/06_finance/finance_info.py b/examples/06_finance/finance_info.py index 2ce90cc..02ee3cc 100644 --- a/examples/06_finance/finance_info.py +++ b/examples/06_finance/finance_info.py @@ -1,21 +1,8 @@ """演示:获取最新财务数据。""" -import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: info = c.get_finance_info(Market.SH, "600519") - df = pd.DataFrame([ - {"项目": "总股本(万股)", "数值": info.zong_guben}, - {"项目": "流通股本(万股)", "数值": info.liutong_guben}, - {"项目": "每股净资产", "数值": info.meigujing_zichan}, - {"项目": "净利润(元)", "数值": info.jing_lirun}, - {"项目": "主营收入(元)", "数值": info.zhuying_shouru}, - {"项目": "主营利润(元)", "数值": info.zhuying_lirun}, - {"项目": "净资产(元)", "数值": info.jing_zichan}, - {"项目": "总资产(元)", "数值": info.zong_zichan}, - {"项目": "股东人数", "数值": info.gudong_renshu}, - {"项目": "上市日期", "数值": info.ipo_date}, - ]) print("贵州茅台 最新财务数据:") - print(df.to_string(index=False, formatters={"数值": lambda x: f"{x:,.0f}"})) + print(info.T.to_string(header=False)) diff --git a/examples/06_finance/price_limits.py b/examples/06_finance/price_limits.py index b77d75d..d49370d 100644 --- a/examples/06_finance/price_limits.py +++ b/examples/06_finance/price_limits.py @@ -1,23 +1,16 @@ """演示:计算个股涨跌停价格。""" -import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient CODE = "600519" NAME = "贵州茅台" with TdxClient.from_best_host() as c: quotes = c.get_security_quotes([(Market.SH, CODE)]) - if quotes: - q = quotes[0] - limit_up, limit_down = c.get_price_limits( - Market.SH, CODE, NAME, q.pre_close - ) - df = pd.DataFrame([{ - "代码": CODE, - "名称": NAME, - "昨收": q.pre_close, - "涨停价": limit_up, - "跌停价": limit_down, - }]) - print(df.to_string(index=False)) + if not quotes.empty: + q = quotes.iloc[0] + limit_up, limit_down = c.get_price_limits(Market.SH, CODE, NAME, q["pre_close"]) + print(f"代码: {CODE} 名称: {NAME}") + print(f"昨收: {q['pre_close']}") + print(f"涨停价: {limit_up}") + print(f"跌停价: {limit_down}") diff --git a/examples/06_finance/xdxr_info.py b/examples/06_finance/xdxr_info.py index 5f0431f..cecea17 100644 --- a/examples/06_finance/xdxr_info.py +++ b/examples/06_finance/xdxr_info.py @@ -1,17 +1,8 @@ """演示:获取除权除息历史记录。""" -import pandas as pd -from easy_tdx import TdxClient, Market, XDXR_CATEGORY_NAMES +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: - records = c.get_xdxr_info(Market.SH, "600519") - df = pd.DataFrame([{ - "日期": f"{r.year}-{r.month:02d}-{r.day:02d}", - "类型": XDXR_CATEGORY_NAMES.get(r.category, f"未知({r.category})"), - "每股分红(元)": r.fenhong, - "送转股比例": r.songzhuangu, - "配股价": r.peigujia, - "配股比例": r.peigu, - } for r in records]) + df = c.get_xdxr_info(Market.SH, "600519") print(f"贵州茅台 除权除息记录,共 {len(df)} 条:") print(df.tail(10).to_string(index=False)) diff --git a/examples/07_block/block_info.py b/examples/07_block/block_info.py index 8cee2ad..2f6af52 100644 --- a/examples/07_block/block_info.py +++ b/examples/07_block/block_info.py @@ -6,16 +6,9 @@ 'block_fg.dat' - 风格板块 """ -import pandas as pd from easy_tdx import TdxClient with TdxClient.from_best_host() as c: - blocks = c.get_block_info("block_gn.dat") - df = pd.DataFrame([{ - "板块名称": b.name, - "分类": b.category, - "成分股数": b.count, - "代码(前5)": ", ".join(b.codes[:5]), - } for b in blocks]) + df = c.get_block_info("block_gn.dat") print(f"概念板块,共 {len(df)} 个:") - print(df.head(20).to_string(index=False)) + print(df[["name", "category", "count"]].head(20).to_string(index=False)) diff --git a/examples/08_fund_flow/fund_flow.py b/examples/08_fund_flow/fund_flow.py index a41b7a8..75da0dc 100644 --- a/examples/08_fund_flow/fund_flow.py +++ b/examples/08_fund_flow/fund_flow.py @@ -4,16 +4,20 @@ """ import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: flow = c.get_fund_flow(Market.SH, "600519") - df = pd.DataFrame([ - {"级别": "超大单", "流入(亿)": flow.super_in / 1e8, "流出(亿)": flow.super_out / 1e8}, - {"级别": "大单", "流入(亿)": flow.large_in / 1e8, "流出(亿)": flow.large_out / 1e8}, - {"级别": "中单", "流入(亿)": flow.medium_in / 1e8, "流出(亿)": flow.medium_out / 1e8}, - {"级别": "小单", "流入(亿)": flow.small_in / 1e8, "流出(亿)": flow.small_out / 1e8}, - ]) + # flow 是单行 DataFrame,转换为万元便于阅读 + in_cols = ["super_in", "large_in", "medium_in", "small_in"] + out_cols = ["super_out", "large_out", "medium_out", "small_out"] + df = pd.DataFrame( + { + "级别": ["超大单", "大单", "中单", "小单"], + "流入(亿)": [flow[c].iloc[0] / 1e8 for c in in_cols], + "流出(亿)": [flow[c].iloc[0] / 1e8 for c in out_cols], + } + ) df["净流入(亿)"] = df["流入(亿)"] - df["流出(亿)"] print("贵州茅台 当日资金流向:") print(df.to_string(index=False)) diff --git a/examples/08_fund_flow/history_fund_flow.py b/examples/08_fund_flow/history_fund_flow.py index 2eae460..f595af9 100644 --- a/examples/08_fund_flow/history_fund_flow.py +++ b/examples/08_fund_flow/history_fund_flow.py @@ -1,15 +1,8 @@ """演示:获取个股历史日线资金流向序列。""" -import pandas as pd -from easy_tdx import TdxClient, Market +from easy_tdx import Market, TdxClient with TdxClient.from_best_host() as c: - flows = c.get_history_fund_flow(Market.SH, "600519", 0, 10) - df = pd.DataFrame([{ - "日期": f"{f.year}-{f.month:02d}-{f.day:02d}", - "超大单净流入(亿)": (f.super_in - f.super_out) / 1e8, - "大单净流入(亿)": (f.large_in - f.large_out) / 1e8, - "主力净流入(亿)": f.main_net_inflow / 1e8, - } for f in flows]) + df = c.get_history_fund_flow(Market.SH, "600519", 0, 10) print(f"贵州茅台 历史资金流向,共 {len(df)} 天:") print(df.to_string(index=False)) diff --git a/examples/09_file_download/report_file.py b/examples/09_file_download/report_file.py index 2d7cbe2..bf3e989 100644 --- a/examples/09_file_download/report_file.py +++ b/examples/09_file_download/report_file.py @@ -59,8 +59,7 @@ with TdxClient.from_best_host() as c: print("行业板块 (block_zs.dat)") print("=" * 50) blocks = c.get_block_info("block_zs.dat") - for b in blocks[:5]: - print(f" {b.name:<10} 分类={b.category} 成分={b.count}") + print(blocks[["name", "category", "count"]].head(5).to_string(index=False)) print(f" ... 共 {len(blocks)} 个") # ── 2. 计算服务器:专业财务数据 ──────────────────────── @@ -74,29 +73,25 @@ calc_host = CALC_HOSTS[0] with TdxClient(calc_host) as c: # 获取文件列表 file_list = c.get_financial_file_list() - for fi in file_list[:5]: - print(f" {fi.filename} {fi.filesize:>12,} 字节 hash={fi.hash[:8]}...") + print(file_list.head(5).to_string(index=False)) print(f" ... 共 {len(file_list)} 个文件") # 下载并解析最近一期有实际数据的财报 - real_files = [f for f in file_list if f.filesize > 10000] - if real_files: - latest = real_files[0] - fname = f"tdxfin/{latest.filename}" - print(f"\n下载: {fname} ({latest.filesize:,} 字节)") + real_files = file_list[file_list["filesize"] > 10000] + if not real_files.empty: + latest = real_files.iloc[0] + fname = f"tdxfin/{latest['filename']}" + print(f"\n下载: {fname} ({latest['filesize']:,} 字节)") # 保存原始 .zip zip_data = c.get_financial_file(fname) - zip_path = OUTPUT_DIR / latest.filename + zip_path = OUTPUT_DIR / latest["filename"] zip_path.write_bytes(zip_data) print(f" .zip 已保存到 {zip_path}") # 解析财报记录 records = c.get_financial_records(fname) print(f" 解析出 {len(records)} 只股票") - if records: - for r in records[:5]: - print(f" {r.market.name} {r.code} 报告期={r.report_date} 字段数={len(r.fields)}") + if not records.empty: + print(records[["market", "code", "report_date"]].head(5).to_string(index=False)) print(f" ... 共 {len(records)} 只") - r = records[0] - print(f" 示例: {r.market.name} {r.code}, 报告期={r.report_date}, 字段数={len(r.fields)}") diff --git a/examples/10_offline/block_data.py b/examples/10_offline/block_data.py index 2f8f52b..7a7d8ec 100644 --- a/examples/10_offline/block_data.py +++ b/examples/10_offline/block_data.py @@ -54,8 +54,15 @@ if need_fetch: print(f"\n正在连接服务器获取 {len(need_fetch)} 个板块文件...") with TdxClient.from_best_host() as c: for name in need_fetch: - blocks = c.get_block_info(name) - _print_blocks(blocks, f"{block_labels[name]} ({name}, 网络)") + df = c.get_block_info(name) + print(f"\n{block_labels[name]} ({name}, 网络) ({len(df)} 个板块):") + for _, row in df.head(5).iterrows(): + codes = row["codes"] + codes_preview = ", ".join(str(c) for c in codes[:5]) + suffix = "..." if len(codes) > 5 else "" + print(f" {row['name']} ({row['count']}只): {codes_preview}{suffix}") + if len(df) > 5: + print(f" ... 还有 {len(df) - 5} 个板块") # --- 自定义板块 --- print(f"\n{'=' * 60}") diff --git a/pyproject.toml b/pyproject.toml index b5ef404..9650f07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,10 @@ version = "1.0.0" description = "通达信 TCP 协议行情数据客户端,支持在线行情与离线本地数据读取" readme = "README.md" requires-python = ">=3.10" -dependencies = ["tzdata>=2024.1"] +dependencies = ["pandas>=2.0", "tzdata>=2024.1"] [project.optional-dependencies] dev = ["pytest>=8.0", "pytest-cov", "mypy>=1.9", "ruff>=0.4"] -pandas = ["pandas>=2.0"] [tool.hatch.build.targets.wheel] packages = ["src/easy_tdx"] diff --git a/src/easy_tdx/_df.py b/src/easy_tdx/_df.py new file mode 100644 index 0000000..9fe80ce --- /dev/null +++ b/src/easy_tdx/_df.py @@ -0,0 +1,111 @@ +"""Dataclass → DataFrame 转换工具。""" + +from __future__ import annotations + +from dataclasses import asdict, is_dataclass +from typing import Any + +import pandas as pd + + +def _to_df(data: Any) -> pd.DataFrame: + """将 list[dataclass] 或单个 dataclass 转为 DataFrame。 + + 自动丢弃以 ``_`` 开头的内部字段(如 ``_raw``)。 + 仅处理 year/month/day(无 hour/minute)→ date 的合并; + SecurityBar 的完整 datetime 合并由调用方按周期决定。 + """ + if isinstance(data, list): + if not data: + return pd.DataFrame() + rows = [] + for item in data: + d = _clean_dict(item) + rows.append(d) + return pd.DataFrame(rows) + if is_dataclass(data) and not isinstance(data, type): + return pd.DataFrame([_clean_dict(data)]) + raise TypeError(f"不支持转换为 DataFrame 的类型: {type(data)}") + + +def _clean_dict(item: Any) -> dict[str, Any]: + d = asdict(item) + d = {k: v for k, v in d.items() if not k.startswith("_")} + return _merge_datetime_fields(d) + + +def _merge_datetime_fields(d: dict[str, Any]) -> dict[str, Any]: + """将仅含 year/month/day(无 hour/minute)的模型合并为 date 列。""" + if all(k in d for k in ("year", "month", "day")) and not all( + k in d for k in ("hour", "minute") + ): + dt = pd.Timestamp(year=d["year"], month=d["month"], day=d["day"]) + result: dict[str, Any] = {"date": dt} + result.update({k: v for k, v in d.items() if k not in {"year", "month", "day"}}) + return result + return d + + +def _merge_bar_datetime(df: pd.DataFrame, daily_plus: bool) -> pd.DataFrame: + """根据 K 线周期将 SecurityBar 的分散字段合并为 date 或 datetime。 + + Args: + daily_plus: True 表示日线及以上周期(DAY/WEEK/MONTH/YEAR),只保留 date; + False 表示分钟线(MIN_1/5/15/30/60),保留完整 datetime。 + """ + if df.empty or "year" not in df.columns: + return df + date_str = ( + df["year"].astype(str) + + "-" + + df["month"].astype(str).str.zfill(2) + + "-" + + df["day"].astype(str).str.zfill(2) + ) + if daily_plus: + df.insert(0, "date", pd.to_datetime(date_str)) + else: + full_str = ( + date_str + + " " + + df["hour"].astype(str).str.zfill(2) + + ":" + + df["minute"].astype(str).str.zfill(2) + ) + df.insert(0, "datetime", pd.to_datetime(full_str)) + df.drop(columns=["year", "month", "day", "hour", "minute"], inplace=True) + return df + + +def _merge_txn_datetime(df: pd.DataFrame, date_int: int) -> pd.DataFrame: + """将逐笔成交的 date + hour:minute 合并为 datetime 列。""" + if df.empty or "hour" not in df.columns: + return df + year = date_int // 10000 + month = (date_int // 100) % 100 + day = date_int % 100 + base = pd.Timestamp(year=year, month=month, day=day) + offsets = pd.to_timedelta(df["hour"] * 3600 + df["minute"] * 60, unit="s") + df.insert(0, "datetime", base + offsets) + df.drop(columns=["hour", "minute"], inplace=True) + return df + + +def _add_minute_datetime(df: pd.DataFrame, date_int: int) -> pd.DataFrame: + """为分时 DataFrame 添加 datetime 列(从 bar 索引计算时间)。 + + A 股分时 240 条:0-119 = 9:30~11:29(上午),120-239 = 13:00~14:59(下午)。 + """ + if df.empty: + return df + year = date_int // 10000 + month = (date_int // 100) % 100 + day = date_int % 100 + base = pd.Timestamp(year=year, month=month, day=day) + n = len(df) + morning = list(range(9 * 60 + 30, 9 * 60 + 30 + 120)) + afternoon = list(range(13 * 60, 13 * 60 + 120)) + all_minutes = (morning + afternoon)[:n] + offsets = pd.to_timedelta(all_minutes, unit="m") + df.insert(0, "datetime", base + offsets) + return df diff --git a/src/easy_tdx/client.py b/src/easy_tdx/client.py index 6f9d1f0..737f9fd 100644 --- a/src/easy_tdx/client.py +++ b/src/easy_tdx/client.py @@ -11,6 +11,9 @@ from types import TracebackType from typing import TypeVar from zoneinfo import ZoneInfo +import pandas as pd + +from ._df import _add_minute_datetime, _merge_bar_datetime, _merge_txn_datetime, _to_df from .codec.block import parse_block_dat from .codec.financial import parse_financial_dat, parse_financial_file_list from .codec.industry import parse_tdxhy_cfg @@ -20,7 +23,7 @@ from .commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd from .commands.company_info import GetCompanyInfoCategoryCmd, GetCompanyInfoContentCmd from .commands.finance_info import GetFinanceInfoCmd from .commands.fund_flow import GetHistoryFundFlowCmd -from .commands.minute_time import GetHistoryMinuteTimeDataCmd, GetMinuteTimeDataCmd +from .commands.minute_time import GetHistoryMinuteTimeDataCmd from .commands.report_file import GetReportFileCmd from .commands.security_bars import GetIndexBarsCmd, GetSecurityBarsCmd from .commands.security_count import GetSecurityCountCmd @@ -32,23 +35,27 @@ from .exceptions import TdxConnectionError from .models.bar import SecurityBar from .models.enums import KlineCategory, Market from .models.finance import ( - CompanyInfoCategory, - FinanceInfo, FinancialFileInfo, FinancialRecord, - TdxBlock, - XdxrRecord, ) -from .models.quote import SecurityQuote from .models.security import SecurityInfo from .models.stats import FundFlow, HistoricalFundFlow, MarketStat -from .models.timeseries import MinuteBar, TransactionRecord +from .models.timeseries import TransactionRecord from .transport.async_ import AsyncTdxConnection from .transport.sync import CALC_HOSTS, KNOWN_HOSTS, TdxConnection, ping_all _DEFAULT_PORT = 7709 _T = TypeVar("_T") _SHANGHAI_TZ = ZoneInfo("Asia/Shanghai") +_DAILY_PLUS = frozenset( + { + KlineCategory.DAY, + KlineCategory.WEEK, + KlineCategory.MONTH, + KlineCategory.YEAR, + KlineCategory.YEAR_ALT, + } +) def _today_in_shanghai() -> int: @@ -88,9 +95,7 @@ def _classify_fund_flow(records: list[TransactionRecord]) -> FundFlow: for record in records: amount = record.price * record.vol * 100.0 - direction = ( - "in" if record.buyorsell == 0 else "out" if record.buyorsell == 1 else None - ) + direction = "in" if record.buyorsell == 0 else "out" if record.buyorsell == 1 else None if not direction: continue @@ -278,11 +283,11 @@ class TdxClient: """获取市场证券总数。""" return self._execute(GetSecurityCountCmd(market)) - def get_security_list(self, market: Market, start: int) -> list[SecurityInfo]: + def get_security_list(self, market: Market, start: int) -> pd.DataFrame: """获取证券列表(每页约1000条,按 start 分页)。""" - return self._execute(GetSecurityListCmd(market, start)) + return _to_df(self._execute(GetSecurityListCmd(market, start))) - def get_security_list_all(self, pages: int | str = "all") -> list[SecurityInfo]: + def get_security_list_all(self, pages: int | str = "all") -> pd.DataFrame: """获取沪深 A 股完整证券列表,并自动挂载行业信息。 Args: @@ -299,7 +304,7 @@ class TdxClient: cached = _load_cache() if cached is not None: log.info("从缓存加载沪深 A 股列表,共 %d 只", len(cached)) - return cached + return _to_df(cached) # 计算每个市场的最大起始偏移 def _max_start(count: int) -> int: @@ -324,15 +329,18 @@ class TdxClient: total_pages = (limit + 999) // 1000 for page_idx, start in enumerate(range(0, limit, 1000)): try: - stocks = self.get_security_list(market, start) + stocks = self._execute(GetSecurityListCmd(market, start)) except Exception: - log.warning("%s 第 %d/%d 页获取失败,跳过", market.name, page_idx + 1, total_pages) + log.warning( + "%s 第 %d/%d 页获取失败,跳过", market.name, page_idx + 1, total_pages + ) continue - log.info("%s 第 %d/%d 页: %d 条", market.name, page_idx + 1, total_pages, len(stocks)) + log.info( + "%s 第 %d/%d 页: %d 条", market.name, page_idx + 1, total_pages, len(stocks) + ) for s in stocks: - is_a_share = ( - (market == Market.SH and s.code.startswith(("60", "68"))) - or (market == Market.SZ and s.code.startswith(("00", "30"))) + is_a_share = (market == Market.SH and s.code.startswith(("60", "68"))) or ( + market == Market.SZ and s.code.startswith(("00", "30")) ) if is_a_share: if s.code in industry_map: @@ -344,13 +352,11 @@ class TdxClient: if pages == "all": _save_cache(all_stocks) - return all_stocks + return _to_df(all_stocks) - def get_security_quotes( - self, stocks: list[tuple[Market, str]] - ) -> list[SecurityQuote]: + def get_security_quotes(self, stocks: list[tuple[Market, str]]) -> pd.DataFrame: """批量获取实时五档行情(最多80只/次)。""" - return self._execute(GetSecurityQuotesCmd(stocks)) + return _to_df(self._execute(GetSecurityQuotesCmd(stocks))) def get_price_limits( self, market: Market, code: str, name: str, pre_close: float @@ -363,8 +369,8 @@ class TdxClient: no_limit_window_days = get_no_limit_window_days(market, code, name) if no_limit_window_days > 0: try: - bars = self.get_security_bars( - market, code, KlineCategory.DAY, 0, no_limit_window_days + 1 + bars = self._execute( + GetSecurityBarsCmd(market, code, KlineCategory.DAY, 0, no_limit_window_days + 1) ) listed_days = len(bars) except Exception: @@ -389,9 +395,10 @@ class TdxClient: category: KlineCategory, start: int, count: int = 800, - ) -> list[SecurityBar]: + ) -> pd.DataFrame: """获取 K 线数据(最多800条/次,按 start 分页)。""" - return self._execute(GetSecurityBarsCmd(market, code, category, start, count)) + df = _to_df(self._execute(GetSecurityBarsCmd(market, code, category, start, count))) + return _merge_bar_datetime(df, category in _DAILY_PLUS) def get_index_bars( self, @@ -400,30 +407,25 @@ class TdxClient: category: KlineCategory, start: int, count: int = 800, - ) -> list[SecurityBar]: + ) -> pd.DataFrame: """获取指数 K 线数据。""" - return self._execute(GetIndexBarsCmd(market, code, category, start, count)) + df = _to_df(self._execute(GetIndexBarsCmd(market, code, category, start, count))) + return _merge_bar_datetime(df, category in _DAILY_PLUS) # ------------------------------------------------------------------ # # 分时 # ------------------------------------------------------------------ # - def get_minute_time_data(self, market: Market, code: str) -> list[MinuteBar]: - """获取今日分时数据(240条)。""" + def get_minute_time_data(self, market: Market, code: str) -> pd.DataFrame: + """获取今日分时数据(240条,走历史分时接口)。""" today = _today_in_shanghai() - try: - bars = self.get_history_minute_time_data(market, code, today) - if bars: - return bars - except Exception: - pass - return self._execute(GetMinuteTimeDataCmd(market, code)) + bars = self._execute(GetHistoryMinuteTimeDataCmd(market, code, today)) + return _add_minute_datetime(_to_df(bars), today) - def get_history_minute_time_data( - self, market: Market, code: str, date: int - ) -> list[MinuteBar]: + def get_history_minute_time_data(self, market: Market, code: str, date: int) -> pd.DataFrame: """获取历史某日分时数据(date: YYYYMMDD)。""" - return self._execute(GetHistoryMinuteTimeDataCmd(market, code, date)) + bars = self._execute(GetHistoryMinuteTimeDataCmd(market, code, date)) + return _add_minute_datetime(_to_df(bars), date) # ------------------------------------------------------------------ # # 逐笔成交 @@ -431,45 +433,41 @@ class TdxClient: def get_transaction_data( self, market: Market, code: str, start: int, count: int = 800 - ) -> list[TransactionRecord]: + ) -> pd.DataFrame: """获取当日逐笔成交(分页)。""" - return self._execute(GetTransactionDataCmd(market, code, start, count)) + df = _to_df(self._execute(GetTransactionDataCmd(market, code, start, count))) + return _merge_txn_datetime(df, _today_in_shanghai()) def get_history_transaction_data( self, market: Market, code: str, date: int, start: int, count: int = 800 - ) -> list[TransactionRecord]: + ) -> pd.DataFrame: """获取历史逐笔成交(date: YYYYMMDD,分页)。""" - return self._execute( - GetHistoryTransactionDataCmd(market, code, date, start, count) - ) + df = _to_df(self._execute(GetHistoryTransactionDataCmd(market, code, date, start, count))) + return _merge_txn_datetime(df, date) # ------------------------------------------------------------------ # # 财务 / 公司 # ------------------------------------------------------------------ # - def get_xdxr_info(self, market: Market, code: str) -> list[XdxrRecord]: + def get_xdxr_info(self, market: Market, code: str) -> pd.DataFrame: """获取除权除息历史记录。""" - return self._execute(GetXdxrInfoCmd(market, code)) + return _to_df(self._execute(GetXdxrInfoCmd(market, code))) - def get_finance_info(self, market: Market, code: str) -> FinanceInfo: + def get_finance_info(self, market: Market, code: str) -> pd.DataFrame: """获取最新财务数据。""" - return self._execute(GetFinanceInfoCmd(market, code)) + return _to_df(self._execute(GetFinanceInfoCmd(market, code))) - def get_company_info_category( - self, market: Market, code: str - ) -> list[CompanyInfoCategory]: + def get_company_info_category(self, market: Market, code: str) -> pd.DataFrame: """获取公司信息文件目录。""" - return self._execute(GetCompanyInfoCategoryCmd(market, code)) + return _to_df(self._execute(GetCompanyInfoCategoryCmd(market, code))) def get_company_info_content( self, market: Market, code: str, filename: str, offset: int, length: int ) -> str: """读取公司信息文本。""" - return self._execute( - GetCompanyInfoContentCmd(market, code, filename, offset, length) - ) + return self._execute(GetCompanyInfoContentCmd(market, code, filename, offset, length)) - def get_block_info(self, filename: str) -> list[TdxBlock]: + def get_block_info(self, filename: str) -> pd.DataFrame: """获取并解析板块文件(行业、概念、风格等)。 常用文件名: @@ -487,7 +485,7 @@ class TdxClient: break full_data.extend(chunk) pos += len(chunk) - return parse_block_dat(bytes(full_data), filename) + return _to_df(parse_block_dat(bytes(full_data), filename)) def get_report_file(self, filename: str) -> bytes: """从服务器拉取大文件(如 'base_info.zip')。""" @@ -527,16 +525,14 @@ class TdxClient: finally: conn.close() - def get_financial_file_list( - self, host: str = CALC_HOSTS[0] - ) -> list[FinancialFileInfo]: + def get_financial_file_list(self, host: str = CALC_HOSTS[0]) -> pd.DataFrame: """获取可用的历史专业财报文件列表。 连接到计算服务器,下载 tdxfin/gpcw.txt 并解析。 """ data = self._download_from_host(host, "tdxfin/gpcw.txt") raw_list = parse_financial_file_list(data) - return [FinancialFileInfo(filename=f, hash=h, filesize=s) for f, h, s in raw_list] + 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: """从计算服务器下载财报 zip 文件。 @@ -546,9 +542,7 @@ class TdxClient: """ return self._download_from_host(host, filename) - def get_financial_records( - self, filename: str, host: str = CALC_HOSTS[0] - ) -> list[FinancialRecord]: + def get_financial_records(self, filename: str, host: str = CALC_HOSTS[0]) -> pd.DataFrame: """下载财报 zip 并解析为每只股票的记录列表。 Args: @@ -560,12 +554,12 @@ class TdxClient: zip_data = self.get_financial_file(filename, host) if not zip_data: - return [] + return pd.DataFrame() with zipfile.ZipFile(io.BytesIO(zip_data)) as zf: dat_names = [n for n in zf.namelist() if n.endswith(".dat")] if not dat_names: - return [] + return pd.DataFrame() dat_data = zf.read(dat_names[0]) m = re.search(r"(\d{8})", filename) @@ -576,13 +570,11 @@ class TdxClient: for code, market_byte, rdate, fields in raw_records: market = Market.SH if market_byte == b"\x01" else Market.SZ records.append( - FinancialRecord( - code=code, market=market, report_date=rdate, fields=fields - ) + FinancialRecord(code=code, market=market, report_date=rdate, fields=fields) ) - return records + return _to_df(records) - def get_market_stat(self) -> MarketStat: + def get_market_stat(self) -> pd.DataFrame: """获取 A 股全市场涨跌统计概况(基于 880005 行情统计)。 注意: @@ -590,9 +582,11 @@ class TdxClient: 用于保证计数守恒,不应视为协议已明确验证的停牌字段。 """ # 通达信中 880005 是全市场行情统计,880001 是总市值指数,880006 是涨跌停统计 - quotes = self.get_security_quotes([ - (Market.SH, "880005"), (Market.SH, "880001"), (Market.SH, "880006"), - ]) + quotes = self._execute( + GetSecurityQuotesCmd( + [(Market.SH, "880005"), (Market.SH, "880001"), (Market.SH, "880006")] + ) + ) if not quotes: raise RuntimeError("无法获取市场统计数据") q = quotes[0] @@ -603,17 +597,19 @@ class TdxClient: market_cap = quotes[1].price * 1e10 if len(quotes) > 1 else 0.0 limit_down = int(quotes[2].open) if len(quotes) > 2 else 0 limit_up = int(quotes[2].price) if len(quotes) > 2 else 0 - return MarketStat( - up_count=up, - down_count=down, - neutral_count=neutral, - suspended_count=max(0, total - up - down - neutral), - total_count=total, - total_amount=q.amount, - total_volume=q.vol, - total_market_cap=market_cap, - limit_up_count=limit_up, - limit_down_count=limit_down, + return _to_df( + MarketStat( + up_count=up, + down_count=down, + neutral_count=neutral, + suspended_count=max(0, total - up - down - neutral), + total_count=total, + total_amount=q.amount, + total_volume=q.vol, + total_market_cap=market_cap, + limit_up_count=limit_up, + limit_down_count=limit_down, + ) ) def _collect_transaction_records( @@ -659,41 +655,43 @@ class TdxClient: return all_recs - def get_fund_flow(self, market: Market, code: str) -> FundFlow: + def get_fund_flow(self, market: Market, code: str) -> pd.DataFrame: """获取个股当日资金流向分布(基于 L1 逐笔数据统计)。""" records = self._collect_transaction_records( - lambda start, page_size: self.get_transaction_data(market, code, start, page_size), + lambda start, page_size: self._execute( + GetTransactionDataCmd(market, code, start, page_size) + ), 2000, ) - return _classify_fund_flow(records) + return _to_df(_classify_fund_flow(records)) def get_history_fund_flow( self, market: Market, code: str, start: int, count: int - ) -> list[HistoricalFundFlow]: + ) -> pd.DataFrame: """获取个股历史日线资金流向序列。 优先走 Category 22 直连接口;若服务器返回空列表,则自动回退为 - “日 K 线取日期 + 历史逐笔成交重算资金流”的兼容实现。 + "日 K 线取日期 + 历史逐笔成交重算资金流"的兼容实现。 """ try: direct = self._execute(GetHistoryFundFlowCmd(market, code, start, count)) except Exception: direct = [] if direct: - return direct + return _to_df(direct) - bars = self.get_security_bars(market, code, KlineCategory.DAY, start, count) + bars = self._execute(GetSecurityBarsCmd(market, code, KlineCategory.DAY, start, count)) results: list[HistoricalFundFlow] = [] for bar in bars: date = _date_from_bar(bar) records = self._collect_transaction_records( - lambda page_start, page_size: self.get_history_transaction_data( - market, code, date, page_start, page_size + lambda page_start, page_size: self._execute( + GetHistoryTransactionDataCmd(market, code, date, page_start, page_size) ), 800, ) results.append(_historical_fund_flow_from_records(date, records)) - return results + return _to_df(results) # ============================================================ @@ -822,10 +820,10 @@ class AsyncTdxClient: async def get_security_count(self, market: Market) -> int: return await self._execute(GetSecurityCountCmd(market)) - async def get_security_list(self, market: Market, start: int) -> list[SecurityInfo]: - return await self._execute(GetSecurityListCmd(market, start)) + async def get_security_list(self, market: Market, start: int) -> pd.DataFrame: + return _to_df(await self._execute(GetSecurityListCmd(market, start))) - async def get_security_list_all(self, pages: int | str = "all") -> list[SecurityInfo]: + async def get_security_list_all(self, pages: int | str = "all") -> pd.DataFrame: """获取沪深 A 股完整证券列表,并自动挂载行业信息。 Args: @@ -842,7 +840,7 @@ class AsyncTdxClient: cached = _load_cache() if cached is not None: log.info("从缓存加载沪深 A 股列表,共 %d 只", len(cached)) - return cached + return _to_df(cached) def _max_start(count: int) -> int: if pages == "all": @@ -865,15 +863,18 @@ class AsyncTdxClient: total_pages = (limit + 999) // 1000 for page_idx, start in enumerate(range(0, limit, 1000)): try: - stocks = await self.get_security_list(market, start) + stocks = await self._execute(GetSecurityListCmd(market, start)) except Exception: - log.warning("%s 第 %d/%d 页获取失败,跳过", market.name, page_idx + 1, total_pages) + log.warning( + "%s 第 %d/%d 页获取失败,跳过", market.name, page_idx + 1, total_pages + ) continue - log.info("%s 第 %d/%d 页: %d 条", market.name, page_idx + 1, total_pages, len(stocks)) + log.info( + "%s 第 %d/%d 页: %d 条", market.name, page_idx + 1, total_pages, len(stocks) + ) for s in stocks: - is_a_share = ( - (market == Market.SH and s.code.startswith(("60", "68"))) - or (market == Market.SZ and s.code.startswith(("00", "30"))) + is_a_share = (market == Market.SH and s.code.startswith(("60", "68"))) or ( + market == Market.SZ and s.code.startswith(("00", "30")) ) if is_a_share: if s.code in industry_map: @@ -883,12 +884,10 @@ class AsyncTdxClient: log.info("沪深 A 股总数: %d", len(all_stocks)) if pages == "all": _save_cache(all_stocks) - return all_stocks + return _to_df(all_stocks) - async def get_security_quotes( - self, stocks: list[tuple[Market, str]] - ) -> list[SecurityQuote]: - return await self._execute(GetSecurityQuotesCmd(stocks)) + async def get_security_quotes(self, stocks: list[tuple[Market, str]]) -> pd.DataFrame: + return _to_df(await self._execute(GetSecurityQuotesCmd(stocks))) async def get_price_limits( self, market: Market, code: str, name: str, pre_close: float @@ -898,8 +897,8 @@ class AsyncTdxClient: no_limit_window_days = get_no_limit_window_days(market, code, name) if no_limit_window_days > 0: try: - bars = await self.get_security_bars( - market, code, KlineCategory.DAY, 0, no_limit_window_days + 1 + bars = await self._execute( + GetSecurityBarsCmd(market, code, KlineCategory.DAY, 0, no_limit_window_days + 1) ) listed_days = len(bars) except Exception: @@ -920,10 +919,9 @@ class AsyncTdxClient: category: KlineCategory, start: int, count: int = 800, - ) -> list[SecurityBar]: - return await self._execute( - GetSecurityBarsCmd(market, code, category, start, count) - ) + ) -> pd.DataFrame: + df = _to_df(await self._execute(GetSecurityBarsCmd(market, code, category, start, count))) + return _merge_bar_datetime(df, category in _DAILY_PLUS) async def get_index_bars( self, @@ -932,55 +930,50 @@ class AsyncTdxClient: category: KlineCategory, start: int, count: int = 800, - ) -> list[SecurityBar]: - return await self._execute(GetIndexBarsCmd(market, code, category, start, count)) + ) -> pd.DataFrame: + df = _to_df(await self._execute(GetIndexBarsCmd(market, code, category, start, count))) + return _merge_bar_datetime(df, category in _DAILY_PLUS) - async def get_minute_time_data(self, market: Market, code: str) -> list[MinuteBar]: + async def get_minute_time_data(self, market: Market, code: str) -> pd.DataFrame: today = _today_in_shanghai() - try: - bars = await self.get_history_minute_time_data(market, code, today) - if bars: - return bars - except Exception: - pass - return await self._execute(GetMinuteTimeDataCmd(market, code)) + bars = await self._execute(GetHistoryMinuteTimeDataCmd(market, code, today)) + return _add_minute_datetime(_to_df(bars), today) async def get_history_minute_time_data( self, market: Market, code: str, date: int - ) -> list[MinuteBar]: - return await self._execute(GetHistoryMinuteTimeDataCmd(market, code, date)) + ) -> pd.DataFrame: + bars = await self._execute(GetHistoryMinuteTimeDataCmd(market, code, date)) + return _add_minute_datetime(_to_df(bars), date) async def get_transaction_data( self, market: Market, code: str, start: int, count: int = 800 - ) -> list[TransactionRecord]: - return await self._execute(GetTransactionDataCmd(market, code, start, count)) + ) -> pd.DataFrame: + df = _to_df(await self._execute(GetTransactionDataCmd(market, code, start, count))) + return _merge_txn_datetime(df, _today_in_shanghai()) async def get_history_transaction_data( self, market: Market, code: str, date: int, start: int, count: int = 800 - ) -> list[TransactionRecord]: - return await self._execute( - GetHistoryTransactionDataCmd(market, code, date, start, count) + ) -> pd.DataFrame: + df = _to_df( + await self._execute(GetHistoryTransactionDataCmd(market, code, date, start, count)) ) + return _merge_txn_datetime(df, date) - async def get_xdxr_info(self, market: Market, code: str) -> list[XdxrRecord]: - return await self._execute(GetXdxrInfoCmd(market, code)) + async def get_xdxr_info(self, market: Market, code: str) -> pd.DataFrame: + return _to_df(await self._execute(GetXdxrInfoCmd(market, code))) - async def get_finance_info(self, market: Market, code: str) -> FinanceInfo: - return await self._execute(GetFinanceInfoCmd(market, code)) + async def get_finance_info(self, market: Market, code: str) -> pd.DataFrame: + return _to_df(await self._execute(GetFinanceInfoCmd(market, code))) - async def get_company_info_category( - self, market: Market, code: str - ) -> list[CompanyInfoCategory]: - return await self._execute(GetCompanyInfoCategoryCmd(market, code)) + async def get_company_info_category(self, market: Market, code: str) -> pd.DataFrame: + return _to_df(await self._execute(GetCompanyInfoCategoryCmd(market, code))) async def get_company_info_content( self, market: Market, code: str, filename: str, offset: int, length: int ) -> str: - return await self._execute( - GetCompanyInfoContentCmd(market, code, filename, offset, length) - ) + return await self._execute(GetCompanyInfoContentCmd(market, code, filename, offset, length)) - async def get_block_info(self, filename: str) -> list[TdxBlock]: + async def get_block_info(self, filename: str) -> pd.DataFrame: """获取并解析板块文件(行业、概念、风格等)。""" size, _hash = await self._execute(GetBlockInfoMetaCmd(filename)) full_data = bytearray() @@ -992,7 +985,7 @@ class AsyncTdxClient: break full_data.extend(chunk) pos += len(chunk) - return parse_block_dat(bytes(full_data), filename) + return _to_df(parse_block_dat(bytes(full_data), filename)) async def get_report_file(self, filename: str) -> bytes: """从服务器拉取大文件。""" @@ -1032,23 +1025,17 @@ class AsyncTdxClient: finally: await conn.close() - async def get_financial_file_list( - self, host: str = CALC_HOSTS[0] - ) -> list[FinancialFileInfo]: + async def get_financial_file_list(self, host: str = CALC_HOSTS[0]) -> pd.DataFrame: """获取可用的历史专业财报文件列表(异步)。""" data = await self._async_download_from_host(host, "tdxfin/gpcw.txt") raw_list = parse_financial_file_list(data) - return [FinancialFileInfo(filename=f, hash=h, filesize=s) for f, h, s in raw_list] + 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 = CALC_HOSTS[0]) -> bytes: """从计算服务器下载财报 zip 文件(异步)。""" return await self._async_download_from_host(host, filename) - async def get_financial_records( - self, filename: str, host: str = CALC_HOSTS[0] - ) -> list[FinancialRecord]: + async def get_financial_records(self, filename: str, host: str = CALC_HOSTS[0]) -> pd.DataFrame: """下载财报 zip 并解析为记录列表(异步)。""" import io import re @@ -1056,12 +1043,12 @@ class AsyncTdxClient: zip_data = await self.get_financial_file(filename, host) if not zip_data: - return [] + return pd.DataFrame() with zipfile.ZipFile(io.BytesIO(zip_data)) as zf: dat_names = [n for n in zf.namelist() if n.endswith(".dat")] if not dat_names: - return [] + return pd.DataFrame() dat_data = zf.read(dat_names[0]) m = re.search(r"(\d{8})", filename) @@ -1072,13 +1059,11 @@ class AsyncTdxClient: for code, market_byte, rdate, fields in raw_records: market = Market.SH if market_byte == b"\x01" else Market.SZ records.append( - FinancialRecord( - code=code, market=market, report_date=rdate, fields=fields - ) + FinancialRecord(code=code, market=market, report_date=rdate, fields=fields) ) - return records + return _to_df(records) - async def get_market_stat(self) -> MarketStat: + async def get_market_stat(self) -> pd.DataFrame: """获取 A 股全市场涨跌统计概况(基于 880005 行情统计)。 注意: @@ -1086,9 +1071,11 @@ class AsyncTdxClient: 用于保证计数守恒,不应视为协议已明确验证的停牌字段。 """ # 通达信中 880005 是全市场行情统计,880001 是总市值指数,880006 是涨跌停统计 - quotes = await self.get_security_quotes([ - (Market.SH, "880005"), (Market.SH, "880001"), (Market.SH, "880006"), - ]) + quotes = await self._execute( + GetSecurityQuotesCmd( + [(Market.SH, "880005"), (Market.SH, "880001"), (Market.SH, "880006")] + ) + ) if not quotes: raise RuntimeError("无法获取市场统计数据") q = quotes[0] @@ -1099,17 +1086,19 @@ class AsyncTdxClient: market_cap = quotes[1].price * 1e10 if len(quotes) > 1 else 0.0 limit_down = int(quotes[2].open) if len(quotes) > 2 else 0 limit_up = int(quotes[2].price) if len(quotes) > 2 else 0 - return MarketStat( - up_count=up, - down_count=down, - neutral_count=neutral, - suspended_count=max(0, total - up - down - neutral), - total_count=total, - total_amount=q.amount, - total_volume=q.vol, - total_market_cap=market_cap, - limit_up_count=limit_up, - limit_down_count=limit_down, + return _to_df( + MarketStat( + up_count=up, + down_count=down, + neutral_count=neutral, + suspended_count=max(0, total - up - down - neutral), + total_count=total, + total_amount=q.amount, + total_volume=q.vol, + total_market_cap=market_cap, + limit_up_count=limit_up, + limit_down_count=limit_down, + ) ) async def _collect_transaction_records( @@ -1155,40 +1144,42 @@ class AsyncTdxClient: return all_recs - async def get_fund_flow(self, market: Market, code: str) -> FundFlow: + async def get_fund_flow(self, market: Market, code: str) -> pd.DataFrame: """获取个股当日资金流向分布(基于 L1 逐笔数据统计)。""" records = await self._collect_transaction_records( - lambda start, page_size: self.get_transaction_data( - market, code, start, page_size + lambda start, page_size: self._execute( + GetTransactionDataCmd(market, code, start, page_size) ), 2000, ) - return _classify_fund_flow(records) + return _to_df(_classify_fund_flow(records)) async def get_history_fund_flow( self, market: Market, code: str, start: int, count: int - ) -> list[HistoricalFundFlow]: + ) -> pd.DataFrame: """获取个股历史日线资金流向序列。 优先走 Category 22 直连接口;若服务器返回空列表,则自动回退为 - “日 K 线取日期 + 历史逐笔成交重算资金流”的兼容实现。 + "日 K 线取日期 + 历史逐笔成交重算资金流"的兼容实现。 """ try: direct = await self._execute(GetHistoryFundFlowCmd(market, code, start, count)) except Exception: direct = [] if direct: - return direct + return _to_df(direct) - bars = await self.get_security_bars(market, code, KlineCategory.DAY, start, count) + bars = await self._execute( + GetSecurityBarsCmd(market, code, KlineCategory.DAY, start, count) + ) results: list[HistoricalFundFlow] = [] for bar in bars: date = _date_from_bar(bar) records = await self._collect_transaction_records( - lambda page_start, page_size: self.get_history_transaction_data( - market, code, date, page_start, page_size + lambda page_start, page_size: self._execute( + GetHistoryTransactionDataCmd(market, code, date, page_start, page_size) ), 800, ) results.append(_historical_fund_flow_from_records(date, records)) - return results + return _to_df(results) diff --git a/src/easy_tdx/commands/minute_time.py b/src/easy_tdx/commands/minute_time.py index 4265d47..ba7d711 100644 --- a/src/easy_tdx/commands/minute_time.py +++ b/src/easy_tdx/commands/minute_time.py @@ -62,7 +62,7 @@ def _parse_minute_body(body: bytes, skip: int = 4) -> list[MinuteBar]: MinuteBar( price=last_price / 100.0, vol=vol, - unknown_1=unknown_1, + _unknown_1=unknown_1, _raw=body[record_start:pos], ) ) diff --git a/src/easy_tdx/models/timeseries.py b/src/easy_tdx/models/timeseries.py index 4b1db4e..e9c96d8 100644 --- a/src/easy_tdx/models/timeseries.py +++ b/src/easy_tdx/models/timeseries.py @@ -10,11 +10,11 @@ class MinuteBar: unknown_1: 协议中第二个变长整数,含义未明(疑似均价的编码形式)。 """ - price: float # 价格 - vol: int # 成交量 + price: float # 价格 + vol: int # 成交量 # pytdx 中被完全丢弃的字段,保留以供分析 - unknown_1: int = field(default=0, repr=False) # 原 reversed1 + _unknown_1: int = field(default=0, repr=False) # 原 reversed1 _raw: bytes = field(default=b"", repr=False, compare=False) diff --git a/tests/fixtures/minute_time.json b/tests/fixtures/minute_time.json index 837d7d6..40963ca 100644 --- a/tests/fixtures/minute_time.json +++ b/tests/fixtures/minute_time.json @@ -3,6 +3,6 @@ "first": { "price": 0.01, "vol": 48, - "unknown_1": 54 + "_unknown_1": 54 } } \ No newline at end of file diff --git a/tests/unit/test_a_share_extensions.py b/tests/unit/test_a_share_extensions.py index d4540ce..b42ae03 100644 --- a/tests/unit/test_a_share_extensions.py +++ b/tests/unit/test_a_share_extensions.py @@ -4,132 +4,175 @@ import asyncio import struct from unittest.mock import patch +import pandas as pd + from easy_tdx import AsyncTdxClient, Market, TdxClient from easy_tdx.client import _classify_fund_flow +from easy_tdx.commands.minute_time import ( + GetHistoryMinuteTimeDataCmd, +) +from easy_tdx.commands.security_bars import GetSecurityBarsCmd +from easy_tdx.commands.security_list import GetSecurityListCmd +from easy_tdx.commands.security_quotes import GetSecurityQuotesCmd +from easy_tdx.commands.transaction import ( + GetHistoryTransactionDataCmd, + GetTransactionDataCmd, +) from easy_tdx.models.bar import SecurityBar from easy_tdx.models.quote import SecurityQuote from easy_tdx.models.security import SecurityInfo -from easy_tdx.models.stats import HistoricalFundFlow -from easy_tdx.models.timeseries import MinuteBar -from easy_tdx.models.timeseries import TransactionRecord +from easy_tdx.models.timeseries import MinuteBar, TransactionRecord @patch("easy_tdx.client.TdxConnection") def test_get_fund_flow_logic(_mock_conn_cls): """测试资金流分类计算逻辑。""" client = TdxClient("127.0.0.1") - - # 构造模拟 Tick 数据 + mock_recs = [ - TransactionRecord(10, 0, 100.0, 101, 0, 0), # super_in (100*101*100 = 101w) - TransactionRecord(10, 1, 10.0, 250, 1, 0), # large_out (10*250*100 = 25w) - TransactionRecord(10, 2, 10.0, 10, 0, 0), # small_in (10*10*100 = 1w) + TransactionRecord(10, 0, 100.0, 101, 0, 0), # super_in + TransactionRecord(10, 1, 10.0, 250, 1, 0), # large_out + TransactionRecord(10, 2, 10.0, 10, 0, 0), # small_in ] - - with patch.object(TdxClient, "get_transaction_data", return_value=mock_recs): + + def mock_execute(cmd): + if isinstance(cmd, GetTransactionDataCmd): + return mock_recs + return [] + + with patch.object(TdxClient, "_execute", side_effect=mock_execute): flow = client.get_fund_flow(Market.SH, "600000") - - assert flow.super_in == 1010000.0 - assert flow.large_out == 250000.0 - assert flow.small_in == 10000.0 - assert flow.main_net_inflow == 1010000.0 - 250000.0 + assert isinstance(flow, pd.DataFrame) + assert flow["super_in"].iloc[0] == 1010000.0 + assert flow["large_out"].iloc[0] == 250000.0 + assert flow["small_in"].iloc[0] == 10000.0 def test_classify_fund_flow_exact_thresholds_use_lower_bucket(): """恰好命中阈值时,应落入较低一档。""" - flow = _classify_fund_flow([ - TransactionRecord(10, 0, 100.0, 100, 0, 0), # 100w -> large - TransactionRecord(10, 1, 100.0, 20, 0, 0), # 20w -> medium - TransactionRecord(10, 2, 100.0, 4, 0, 0), # 4w -> small - ]) + flow = _classify_fund_flow( + [ + TransactionRecord(10, 0, 100.0, 100, 0, 0), # 100w -> large + TransactionRecord(10, 1, 100.0, 20, 0, 0), # 20w -> medium + TransactionRecord(10, 2, 100.0, 4, 0, 0), # 4w -> small + ] + ) assert flow.super_in == 0.0 assert flow.large_in == 1000000.0 assert flow.medium_in == 200000.0 assert flow.small_in == 40000.0 + @patch("easy_tdx.client.TdxConnection") def test_get_security_list_all_filtering(_mock_conn_cls): """测试三市 A 股过滤与行业挂载逻辑。""" client = TdxClient("127.0.0.1") - - # 模拟行业配置 tdxhy.cfg + industry_cfg = b"1|600000|T01|||X01\n0|000001|T02|||X02\n2|830000|T03|||X03" - - # 模拟各市场返回 - def mock_get_list(market, start): - if market == Market.SH: - return [ - SecurityInfo(Market.SH, "600000", "SH_A", 100, 2, 10.0), - SecurityInfo(Market.SH, "999999", "INDEX", 100, 2, 3000.0), # 应被过滤 - ] - if market == Market.SZ: - return [SecurityInfo(Market.SZ, "000001", "SZ_A", 100, 2, 10.0)] - if market == Market.BJ: - return [SecurityInfo(Market.BJ, "830000", "BJ_A", 100, 2, 10.0)] + + def mock_execute(cmd): + if isinstance(cmd, GetSecurityListCmd): + if cmd.market == Market.SH: + return [ + SecurityInfo(Market.SH, "600000", "SH_A", 100, 2, 10.0), + SecurityInfo(Market.SH, "999999", "INDEX", 100, 2, 3000.0), + ] + if cmd.market == Market.SZ: + return [SecurityInfo(Market.SZ, "000001", "SZ_A", 100, 2, 10.0)] + return [] return [] - with patch.object(TdxClient, "get_report_file", return_value=industry_cfg), \ - patch.object(TdxClient, "get_security_count", return_value=1), \ - patch.object(TdxClient, "get_security_list", side_effect=mock_get_list): - - all_stocks = client.get_security_list_all() + with ( + patch.object(TdxClient, "_execute", side_effect=mock_execute), + patch.object(TdxClient, "get_report_file", return_value=industry_cfg), + patch.object(TdxClient, "get_security_count", return_value=1), + ): + all_stocks = client.get_security_list_all(pages=1) - # 预期只有 SH 和 SZ,BJ 已在扫描中降级移除 + assert isinstance(all_stocks, pd.DataFrame) assert len(all_stocks) == 2 - codes = [s.code for s in all_stocks] + codes = all_stocks["code"].tolist() assert "600000" in codes assert "000001" in codes - assert "830000" not in codes - s0 = next(s for s in all_stocks if s.code == "600000") - assert s0.industry_tdx == "T01" + assert "830000" not in codes + row = all_stocks[all_stocks["code"] == "600000"].iloc[0] + assert row["industry_tdx"] == "T01" + @patch("easy_tdx.client.TdxConnection") def test_get_market_stat_mapping(_mock_conn_cls): """测试市场统计字段映射。""" client = TdxClient("127.0.0.1") - + mock_quote = SecurityQuote( - Market.SH, "880005", - price=3000.0, # up - pre_close=2000.0, # down - open=0, - high=5500.0, # total - low=500.0, # neutral (low=500 -> neutral_count=500) - vol=1000000.0, cur_vol=0, amount=50000000.0, - s_vol=0, b_vol=0, active1=0, active2=0, - bid1=0, bid_vol1=0, bid2=0, bid_vol2=0, bid3=0, bid_vol3=0, - bid4=0, bid_vol4=0, bid5=0, bid_vol5=0, - ask1=0, ask_vol1=0, ask2=0, ask_vol2=0, ask3=0, ask_vol3=0, - ask4=0, ask_vol4=0, ask5=0, ask_vol5=0, - rise_speed=0, limit_up=0, limit_down=0 + Market.SH, + "880005", + price=3000.0, # up = int(price) + pre_close=0, + open=2000.0, # down = int(open) + high=5500.0, # total = int(high) + low=500.0, # neutral = int(low) + vol=1000000.0, + cur_vol=0, + amount=50000000.0, + s_vol=0, + b_vol=0, + active1=0, + active2=0, + bid1=0, + bid_vol1=0, + bid2=0, + bid_vol2=0, + bid3=0, + bid_vol3=0, + bid4=0, + bid_vol4=0, + bid5=0, + bid_vol5=0, + ask1=0, + ask_vol1=0, + ask2=0, + ask_vol2=0, + ask3=0, + ask_vol3=0, + ask4=0, + ask_vol4=0, + ask5=0, + ask_vol5=0, + rise_speed=0, + limit_up=0, + limit_down=0, ) - - with patch.object(TdxClient, "get_security_quotes", return_value=[mock_quote]): + + def mock_execute(cmd): + if isinstance(cmd, GetSecurityQuotesCmd): + return [mock_quote] + return [] + + with patch.object(TdxClient, "_execute", side_effect=mock_execute): stat = client.get_market_stat() - assert stat.up_count == 3000 - assert stat.down_count == 2000 - assert stat.neutral_count == 500 - assert stat.total_count == 5500 + assert isinstance(stat, pd.DataFrame) + assert stat["up_count"].iloc[0] == 3000 + assert stat["down_count"].iloc[0] == 2000 + assert stat["neutral_count"].iloc[0] == 500 + assert stat["total_count"].iloc[0] == 5500 + def test_get_history_fund_flow_parsing(): """测试历史资金流序列解析逻辑。""" from easy_tdx.commands.fund_flow import GetHistoryFundFlowCmd - - # 模拟 Category 22 响应 (Header 9 + Count 2 + Body 36) + body = bytearray(9) - body.extend(struct.pack(" 0: + def mock_execute(cmd): + if isinstance(cmd, GetHistoryFundFlowCmd): return [] - return txn_map[date] + if isinstance(cmd, GetSecurityBarsCmd): + return bars + if isinstance(cmd, GetHistoryTransactionDataCmd): + if cmd.start > 0: + return [] + return txn_map.get(cmd.date, []) + return [] - with patch.object(TdxClient, "_execute", return_value=[]), patch.object( - TdxClient, "get_security_bars", return_value=bars - ), patch.object( - TdxClient, "get_history_transaction_data", side_effect=mock_history_txn - ): + with patch.object(TdxClient, "_execute", side_effect=mock_execute): flows = client.get_history_fund_flow(Market.SH, "600000", 0, 2) - assert flows == [ - HistoricalFundFlow( - year=2025, - month=1, - day=8, - super_in=1010000.0, - super_out=0.0, - large_in=0.0, - large_out=250000.0, - medium_in=0.0, - medium_out=0.0, - small_in=0.0, - small_out=0.0, - ), - HistoricalFundFlow( - year=2025, - month=1, - day=9, - super_in=0.0, - super_out=0.0, - large_in=0.0, - large_out=0.0, - medium_in=0.0, - medium_out=0.0, - small_in=10000.0, - small_out=0.0, - ), - ] + assert isinstance(flows, pd.DataFrame) + assert len(flows) == 2 + row0 = flows.iloc[0] + assert row0["super_in"] == 1010000.0 + assert row0["large_out"] == 250000.0 + row1 = flows.iloc[1] + assert row1["small_in"] == 10000.0 @patch("easy_tdx.client.TdxConnection") @@ -202,21 +228,23 @@ def test_get_price_limits_uses_listing_window(_mock_conn_cls): """client.get_price_limits 应结合日 K 条数判断上市初期限价窗口。""" client = TdxClient("127.0.0.1") - with patch.object( - TdxClient, - "get_security_bars", - return_value=[SecurityBar(0, 0, 0, 0, 0, 0, 2025, 1, 1, 15, 0)] * 5, - ): + def mock_execute_5(cmd): + if isinstance(cmd, GetSecurityBarsCmd): + return [SecurityBar(0, 0, 0, 0, 0, 0, 2025, 1, 1, 15, 0)] * 5 + return [] + + with patch.object(TdxClient, "_execute", side_effect=mock_execute_5): assert client.get_price_limits(Market.SH, "600001", "主板新股", 10.0) == ( None, None, ) - with patch.object( - TdxClient, - "get_security_bars", - return_value=[SecurityBar(0, 0, 0, 0, 0, 0, 2025, 1, 1, 15, 0)] * 6, - ): + def mock_execute_6(cmd): + if isinstance(cmd, GetSecurityBarsCmd): + return [SecurityBar(0, 0, 0, 0, 0, 0, 2025, 1, 1, 15, 0)] * 6 + return [] + + with patch.object(TdxClient, "_execute", side_effect=mock_execute_6): assert client.get_price_limits(Market.SH, "600001", "主板老股", 10.0) == ( 11.0, 9.0, @@ -224,67 +252,50 @@ def test_get_price_limits_uses_listing_window(_mock_conn_cls): @patch("easy_tdx.client.TdxConnection") -def test_get_minute_time_data_prefers_history_endpoint(_mock_conn_cls): - """今日分时优先走历史分时接口,规避当前分时协议歧义。""" +def test_get_minute_time_data_uses_history_endpoint(_mock_conn_cls): + """今日分时走历史分时接口。""" client = TdxClient("127.0.0.1") expected = [MinuteBar(price=9.7, vol=13694)] - with patch("easy_tdx.client._today_in_shanghai", return_value=20260422), patch.object( - TdxClient, - "get_history_minute_time_data", - return_value=expected, - ) as mock_history, patch.object( - TdxClient, - "_execute", - side_effect=AssertionError("should not hit current-minute command"), + def mock_execute(cmd): + if isinstance(cmd, GetHistoryMinuteTimeDataCmd): + return expected + return [] + + with ( + patch("easy_tdx.client._today_in_shanghai", return_value=20260422), + patch.object(TdxClient, "_execute", side_effect=mock_execute) as mock_exec, ): result = client.get_minute_time_data(Market.SH, "600000") - mock_history.assert_called_once_with(Market.SH, "600000", 20260422) - assert result == expected + assert isinstance(result, pd.DataFrame) + assert result["price"].iloc[0] == 9.7 + history_calls = [ + c for c in mock_exec.call_args_list if isinstance(c[0][0], GetHistoryMinuteTimeDataCmd) + ] + assert len(history_calls) == 1 -@patch("easy_tdx.client.TdxConnection") -def test_get_minute_time_data_falls_back_to_current_endpoint(_mock_conn_cls): - """历史分时失败时,仍回退到原今日分时命令。""" - client = TdxClient("127.0.0.1") - fallback = [MinuteBar(price=9.61, vol=10698)] - - with patch("easy_tdx.client._today_in_shanghai", return_value=20260422), patch.object( - TdxClient, - "get_history_minute_time_data", - side_effect=RuntimeError("history unavailable"), - ) as mock_history, patch.object( - TdxClient, - "_execute", - return_value=fallback, - ) as mock_execute: - result = client.get_minute_time_data(Market.SH, "600000") - - mock_history.assert_called_once_with(Market.SH, "600000", 20260422) - mock_execute.assert_called_once() - assert result == fallback - - -def test_async_get_minute_time_data_prefers_history_endpoint(): - """异步客户端应与同步客户端保持同一回退策略。""" +def test_async_get_minute_time_data_uses_history_endpoint(): + """异步客户端走历史分时接口。""" expected = [MinuteBar(price=9.7, vol=13694)] async def run_test() -> None: with patch("easy_tdx.client.AsyncTdxConnection"): client = AsyncTdxClient("127.0.0.1") - with patch("easy_tdx.client._today_in_shanghai", return_value=20260422), patch.object( - AsyncTdxClient, - "get_history_minute_time_data", - return_value=expected, - ) as mock_history, patch.object( - AsyncTdxClient, - "_execute", - side_effect=AssertionError("should not hit current-minute command"), + + async def mock_execute(cmd): + if isinstance(cmd, GetHistoryMinuteTimeDataCmd): + return expected + return [] + + with ( + patch("easy_tdx.client._today_in_shanghai", return_value=20260422), + patch.object(AsyncTdxClient, "_execute", side_effect=mock_execute), ): result = await client.get_minute_time_data(Market.SH, "600000") - mock_history.assert_called_once_with(Market.SH, "600000", 20260422) - assert result == expected + assert isinstance(result, pd.DataFrame) + assert result["price"].iloc[0] == 9.7 asyncio.run(run_test()) diff --git a/tests/unit/test_block_info.py b/tests/unit/test_block_info.py index 23d7c26..f4786cd 100644 --- a/tests/unit/test_block_info.py +++ b/tests/unit/test_block_info.py @@ -4,6 +4,8 @@ import asyncio import struct from unittest.mock import patch +import pandas as pd + from easy_tdx.client import AsyncTdxClient, TdxClient from easy_tdx.codec.block import parse_block_dat from easy_tdx.models.finance import TdxBlock @@ -13,10 +15,11 @@ from easy_tdx.models.finance import TdxBlock def test_async_get_block_info_logic(mock_conn_cls): """测试 AsyncTdxClient.get_block_info 的异步拉取逻辑。""" mock_conn = mock_conn_cls.return_value - + # 模拟异步 execute async def mock_execute(cmd): from easy_tdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd + if isinstance(cmd, GetBlockInfoMetaCmd): return 100, "hash" if isinstance(cmd, GetBlockInfoCmd): @@ -32,9 +35,9 @@ def test_async_get_block_info_logic(mock_conn_cls): with patch("easy_tdx.client.parse_block_dat") as mock_parse: mock_parse.return_value = [] res = await client.get_block_info("test.dat") - - assert isinstance(res, list) - assert mock_conn.execute.call_count == 2 # 1 meta + 1 data + + assert isinstance(res, pd.DataFrame) + assert mock_conn.execute.call_count == 2 # 1 meta + 1 data asyncio.run(main()) @@ -49,20 +52,20 @@ def test_parse_block_dat_basic(): # Header 384 + Count 2 + Record 2813 data = bytearray(384) data.extend(struct.pack(" bytes: # security_count # --------------------------------------------------------------------------- + def test_security_count_parse(): from easy_tdx.commands.security_count import GetSecurityCountCmd from easy_tdx.models.enums import Market @@ -38,6 +40,7 @@ def test_security_count_parse(): # security_list # --------------------------------------------------------------------------- + def test_security_list_parse(): from easy_tdx.commands.security_list import GetSecurityListCmd from easy_tdx.models.enums import Market @@ -61,18 +64,15 @@ def test_security_list_pre_close_uses_tdx_float_for_a_share(): from easy_tdx.commands.security_list import GetSecurityListCmd from easy_tdx.models.enums import Market - body = ( - struct.pack(" 0 # fixed values assert abs(b0.price - 0.01) < 0.001 assert b0.vol == 48 - assert b0.unknown_1 == 54 + assert b0._unknown_1 == 54 # --------------------------------------------------------------------------- # history_minute_time # --------------------------------------------------------------------------- + def test_history_minute_time_parse(): from easy_tdx.commands.minute_time import GetHistoryMinuteTimeDataCmd from easy_tdx.models.enums import Market @@ -202,7 +206,7 @@ def test_history_minute_time_parse(): b0 = bars[0] assert abs(b0.price - 10.29) < 0.01 assert b0.vol == 10044 - assert hasattr(b0, "unknown_1") + assert hasattr(b0, "_unknown_1") assert len(b0._raw) > 0 @@ -210,6 +214,7 @@ def test_history_minute_time_parse(): # transaction (current day) # --------------------------------------------------------------------------- + def test_transaction_parse(): from easy_tdx.commands.transaction import GetTransactionDataCmd from easy_tdx.models.enums import Market @@ -239,6 +244,7 @@ def test_transaction_parse(): # history_transaction # --------------------------------------------------------------------------- + def test_history_transaction_parse(): from easy_tdx.commands.transaction import GetHistoryTransactionDataCmd from easy_tdx.models.enums import Market @@ -266,6 +272,7 @@ def test_history_transaction_parse(): # xdxr_info # --------------------------------------------------------------------------- + def test_xdxr_info_parse(): from easy_tdx.commands.xdxr_info import GetXdxrInfoCmd from easy_tdx.models.enums import Market @@ -329,6 +336,7 @@ def test_xdxr_info_category_1_normalizes_per_10_share_fields(): # finance_info # --------------------------------------------------------------------------- + def test_finance_info_parse(): from easy_tdx.commands.finance_info import GetFinanceInfoCmd from easy_tdx.models.enums import Market @@ -354,6 +362,7 @@ def test_finance_info_parse(): # company_info_category # --------------------------------------------------------------------------- + def test_company_info_category_parse(): from easy_tdx.commands.company_info import GetCompanyInfoCategoryCmd from easy_tdx.models.enums import Market @@ -375,6 +384,7 @@ def test_company_info_category_parse(): # company_info_content # --------------------------------------------------------------------------- + def test_company_info_content_parse(): from easy_tdx.commands.company_info import GetCompanyInfoContentCmd from easy_tdx.models.enums import Market From f693cbe2181ceeb48d27cf8722d4794f7cef0216 Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Fri, 22 May 2026 04:22:27 +0800 Subject: [PATCH 2/5] docs: update README to reflect merged datetime fields in DataFrame output --- README.md | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a24da9f..0f2b079 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,10 @@ with TdxClient("180.153.18.170") as c: # 自动优选最低延迟服务器 with TdxClient.from_best_host() as c: - bars = c.get_security_bars(Market.SH, "600000", KlineCategory.DAY, 0, 5) - for b in bars: - print(f"{b.year}-{b.month:02d}-{b.day:02d} " - f"开:{b.open:.2f} 高:{b.high:.2f} " - f"低:{b.low:.2f} 收:{b.close:.2f}") + df = c.get_security_bars(Market.SH, "600000", KlineCategory.DAY, 0, 5) + print(df.to_string(index=False)) + # date open close high low vol amount + # 2025-01-02 10.25 10.12 10.25 10.08 108154752.0 1.078280e+09 ``` ### asyncio @@ -60,13 +59,10 @@ from easy_tdx import AsyncTdxClient, Market, KlineCategory async def main(): async with AsyncTdxClient.from_best_host() as c: - bars = await c.get_security_bars( + df = await c.get_security_bars( Market.SH, "600000", KlineCategory.DAY, 0, 5 ) - for bar in bars: - print(f"{bar.year}-{bar.month:02d}-{bar.day:02d} " - f"开:{bar.open:.2f} 高:{bar.high:.2f} " - f"低:{bar.low:.2f} 收:{bar.close:.2f}") + print(df.to_string(index=False)) asyncio.run(main()) ``` @@ -143,7 +139,7 @@ KlineCategory.MIN_1 MIN_5 MIN_15 MIN_30 MIN_60 KlineCategory.DAY WEEK MONTH YEAR ``` -K 线字段:`open` `close` `high` `low` `vol` `amount` `year` `month` `day` `hour` `minute` `_raw` +K 线字段:`date`(日线及以上)或 `datetime`(分钟线) `open` `close` `high` `low` `vol` `amount` ### 分时数据 @@ -156,7 +152,7 @@ with TdxClient.from_best_host() as c: bars = c.get_history_minute_time_data(Market.SH, "600000", 20250110) ``` -分时字段:`price` `vol` `unknown_1`(原 pytdx 丢弃字段,保留供分析)`_raw` +分时字段:`datetime` `price` `vol` ### 逐笔成交 @@ -169,7 +165,7 @@ with TdxClient.from_best_host() as c: records = c.get_history_transaction_data(Market.SH, "600000", 20250110, 0, 20) ``` -成交字段:`hour` `minute` `price` `vol` `buyorsell`(0=买, 1=卖, 2=中性, 8=集合竞价)`unknown_last` `_raw` +成交字段:`datetime` `price` `vol` `buyorsell`(0=买, 1=卖, 2=中性, 8=集合竞价) ### 财务与公司信息 @@ -225,7 +221,7 @@ with TdxClient.from_best_host() as c: # 历史日线资金流向序列 flows = c.get_history_fund_flow(Market.SH, "600519", 0, 10) - # flows[0].year / .month / .day / .super_in / .main_net_inflow + # flows[0].date / .super_in / .main_net_inflow ``` ### 文件下载 @@ -475,9 +471,8 @@ vipdoc/ ### SecurityBar(K 线) ``` +date(日线及以上)或 datetime(分钟线) open close high low vol amount -year month day hour minute -_raw ``` ### SecurityQuote(实时行情) @@ -503,19 +498,19 @@ industry_tdx industry_sw ### MinuteBar(分时) ``` -price vol unknown_1 _raw +datetime price vol ``` ### TransactionRecord(逐笔成交) ``` -hour minute price vol buyorsell unknown_last _raw +datetime price vol buyorsell ``` ### XdxrRecord(除权除息) ``` -market code year month day category name +date market code category name fenhong peigujia songzhuangu peigu suogu xingquanjia fenshu panqian_liutong panhou_liutong # 万股 From 0e8eba0cfdf0cf4017a286a0dd02cda29ef2699e Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 22 May 2026 13:14:45 +0800 Subject: [PATCH 3/5] chore: remove .omc directory from tracking and add to .gitignore Co-Authored-By: Claude Opus 4.7 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9891dad..9fdb542 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ build/ *.egg .venv/ venv/ +.omc/ From 67a0415c38ef4c78ad720da5ccd3d3a1402a8d5f Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 22 May 2026 13:25:15 +0800 Subject: [PATCH 4/5] chore: remove uv.lock and add to .gitignore Co-Authored-By: Claude Opus 4.7 --- .gitignore | 1 + uv.lock | 872 ----------------------------------------------------- 2 files changed, 1 insertion(+), 872 deletions(-) delete mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 9fdb542..6faecdb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ build/ .venv/ venv/ .omc/ +uv.lock diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 4a35e6d..0000000 --- a/uv.lock +++ /dev/null @@ -1,872 +0,0 @@ -version = 1 -revision = 1 -requires-python = ">=3.10" -resolution-markers = [ - "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version >= '3.15' and sys_platform == 'emscripten'", - "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'emscripten'", - "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.11'", -] - -[[package]] -name = "ast-serialize" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520 }, - { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779 }, - { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750 }, - { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942 }, - { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517 }, - { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081 }, - { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910 }, - { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678 }, - { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603 }, - { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332 }, - { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979 }, - { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002 }, - { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231 }, - { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668 }, - { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075 }, - { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347 }, - { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380 }, - { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879 }, - { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529 }, - { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560 }, - { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172 }, - { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072 }, - { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488 }, - { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702 }, - { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182 }, - { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410 }, - { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587 }, - { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171 }, - { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668 }, - { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311 }, - { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931 }, - { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "coverage" -version = "7.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668 }, - { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192 }, - { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932 }, - { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762 }, - { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625 }, - { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539 }, - { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636 }, - { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666 }, - { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670 }, - { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484 }, - { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942 }, - { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544 }, - { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285 }, - { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215 }, - { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795 }, - { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299 }, - { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721 }, - { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633 }, - { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743 }, - { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700 }, - { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854 }, - { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433 }, - { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494 }, - { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261 }, - { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216 }, - { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125 }, - { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300 }, - { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241 }, - { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908 }, - { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967 }, - { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329 }, - { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839 }, - { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576 }, - { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690 }, - { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949 }, - { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242 }, - { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608 }, - { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753 }, - { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823 }, - { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323 }, - { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197 }, - { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515 }, - { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324 }, - { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944 }, - { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990 }, - { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365 }, - { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363 }, - { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961 }, - { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193 }, - { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326 }, - { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582 }, - { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325 }, - { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291 }, - { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448 }, - { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110 }, - { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885 }, - { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539 }, - { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344 }, - { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966 }, - { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679 }, - { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033 }, - { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333 }, - { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410 }, - { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836 }, - { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974 }, - { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578 }, - { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394 }, - { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022 }, - { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732 }, - { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921 }, - { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109 }, - { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212 }, - { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272 }, - { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530 }, - { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036 }, - { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368 }, - { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417 }, - { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924 }, - { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269 }, - { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583 }, - { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434 }, - { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280 }, - { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241 }, - { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516 }, - { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059 }, - { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716 }, - { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788 }, - { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600 }, - { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168 }, - { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766 }, - { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035 }, - { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405 }, - { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530 }, - { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932 }, - { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062 }, - { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504 }, - { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398 }, - { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000 }, - { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732 }, - { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847 }, - { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166 }, - { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573 }, - { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680 }, - { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703 }, - { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764 }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, -] - -[[package]] -name = "librt" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706 }, - { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605 }, - { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555 }, - { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434 }, - { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918 }, - { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334 }, - { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287 }, - { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202 }, - { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517 }, - { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878 }, - { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070 }, - { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918 }, - { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092 }, - { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035 }, - { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022 }, - { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273 }, - { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083 }, - { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139 }, - { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442 }, - { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230 }, - { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231 }, - { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585 }, - { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509 }, - { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628 }, - { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122 }, - { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147 }, - { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614 }, - { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538 }, - { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623 }, - { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082 }, - { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105 }, - { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268 }, - { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348 }, - { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294 }, - { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608 }, - { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879 }, - { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831 }, - { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470 }, - { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119 }, - { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565 }, - { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395 }, - { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383 }, - { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010 }, - { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433 }, - { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595 }, - { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255 }, - { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847 }, - { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920 }, - { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898 }, - { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812 }, - { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448 }, - { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345 }, - { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131 }, - { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024 }, - { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221 }, - { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174 }, - { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216 }, - { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921 }, - { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850 }, - { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237 }, - { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261 }, - { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965 }, - { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151 }, - { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850 }, - { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138 }, - { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976 }, - { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927 }, - { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698 }, - { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162 }, - { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494 }, - { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858 }, - { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318 }, - { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115 }, - { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918 }, - { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562 }, - { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327 }, - { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572 }, -] - -[[package]] -name = "mypy" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ast-serialize" }, - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792 }, - { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739 }, - { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199 }, - { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128 }, - { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378 }, - { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994 }, - { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743 }, - { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685 }, - { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165 }, - { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376 }, - { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618 }, - { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063 }, - { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564 }, - { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983 }, - { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381 }, - { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501 }, - { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750 }, - { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630 }, - { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831 }, - { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228 }, - { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684 }, - { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174 }, - { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542 }, - { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929 }, - { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200 }, - { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690 }, - { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435 }, - { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052 }, - { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422 }, - { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374 }, - { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743 }, - { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937 }, - { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371 }, - { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429 }, - { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799 }, - { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458 }, - { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697 }, - { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638 }, - { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852 }, - { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695 }, - { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622 }, - { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798 }, - { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, -] - -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, -] - -[[package]] -name = "numpy" -version = "2.4.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version >= '3.15' and sys_platform == 'emscripten'", - "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'emscripten'", - "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194 }, - { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111 }, - { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159 }, - { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936 }, - { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692 }, - { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164 }, - { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877 }, - { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487 }, - { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945 }, - { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406 }, - { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528 }, - { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119 }, - { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246 }, - { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410 }, - { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240 }, - { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012 }, - { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538 }, - { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706 }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541 }, - { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825 }, - { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687 }, - { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482 }, - { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648 }, - { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902 }, - { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992 }, - { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944 }, - { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392 }, - { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220 }, - { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800 }, - { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600 }, - { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134 }, - { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598 }, - { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272 }, - { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197 }, - { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287 }, - { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763 }, - { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070 }, - { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752 }, - { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024 }, - { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398 }, - { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971 }, - { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532 }, - { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881 }, - { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458 }, - { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559 }, - { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716 }, - { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947 }, - { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197 }, - { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245 }, - { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587 }, - { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226 }, - { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196 }, - { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334 }, - { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678 }, - { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672 }, - { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731 }, - { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805 }, - { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496 }, - { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616 }, - { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145 }, - { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813 }, - { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982 }, - { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908 }, - { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867 }, - { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511 }, - { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064 }, - { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157 }, - { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728 }, - { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374 }, - { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286 }, - { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263 }, -] - -[[package]] -name = "packaging" -version = "26.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195 }, -] - -[[package]] -name = "pandas" -version = "2.3.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763 }, - { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217 }, - { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791 }, - { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373 }, - { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444 }, - { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459 }, - { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086 }, - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790 }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831 }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267 }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281 }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453 }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361 }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702 }, - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846 }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618 }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212 }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693 }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002 }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971 }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722 }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175 }, -] - -[[package]] -name = "pandas" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version >= '3.15' and sys_platform == 'emscripten'", - "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'emscripten'", - "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495 }, - { url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250 }, - { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558 }, - { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611 }, - { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670 }, - { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708 }, - { url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609 }, - { url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596 }, - { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846 }, - { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550 }, - { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965 }, - { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600 }, - { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824 }, - { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889 }, - { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463 }, - { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158 }, - { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071 }, - { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690 }, - { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634 }, - { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243 }, - { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659 }, - { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880 }, - { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091 }, - { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282 }, - { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016 }, - { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210 }, - { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126 }, - { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051 }, - { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796 }, - { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741 }, - { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958 }, - { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065 }, - { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101 }, - { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553 }, - { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065 }, - { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188 }, - { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966 }, - { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755 }, - { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658 }, - { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242 }, - { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369 }, - { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306 }, - { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394 }, - { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717 }, - { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897 }, - { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855 }, - { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464 }, -] - -[[package]] -name = "pathspec" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328 }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, -] - -[[package]] -name = "pytest" -version = "9.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249 }, -] - -[[package]] -name = "pytest-cov" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876 }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - -[[package]] -name = "pytz" -version = "2026.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141 }, -] - -[[package]] -name = "ruff" -version = "0.15.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279 }, - { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798 }, - { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761 }, - { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451 }, - { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285 }, - { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063 }, - { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079 }, - { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833 }, - { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486 }, - { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189 }, - { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380 }, - { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605 }, - { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554 }, - { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133 }, - { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455 }, - { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409 }, - { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336 }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704 }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454 }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561 }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824 }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227 }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859 }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204 }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084 }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285 }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924 }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018 }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948 }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341 }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159 }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290 }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141 }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847 }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088 }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866 }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887 }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704 }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628 }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180 }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674 }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976 }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755 }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265 }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726 }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859 }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713 }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084 }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973 }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223 }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973 }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082 }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490 }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263 }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736 }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717 }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461 }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855 }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144 }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683 }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196 }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393 }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583 }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, -] - -[[package]] -name = "tzdata" -version = "2026.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321 }, -] - -[[package]] -name = "xmtdx" -version = "0.1.1" -source = { editable = "." } -dependencies = [ - { name = "tzdata" }, -] - -[package.optional-dependencies] -dev = [ - { name = "mypy" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] -pandas = [ - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] - -[package.metadata] -requires-dist = [ - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.9" }, - { name = "pandas", marker = "extra == 'pandas'", specifier = ">=2.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, - { name = "pytest-cov", marker = "extra == 'dev'" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" }, - { name = "tzdata", specifier = ">=2024.1" }, -] -provides-extras = ["dev", "pandas"] From 4820b4a0496899ece0b8ca4d7f4d66a5159da7f8 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 22 May 2026 22:44:45 +0800 Subject: [PATCH 5/5] 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 --- README.md | 950 ++++++------- examples/01_connection/async_connect.py | 49 +- examples/01_connection/connect_best_host.py | 39 +- examples/01_connection/ping_servers.py | 44 +- examples/02_market_info/market_stat.py | 35 +- examples/02_market_info/security_count.py | 25 +- examples/02_market_info/security_list.py | 63 +- examples/02_market_info/security_list_all.py | 80 +- examples/02_market_info/security_quotes.py | 50 +- examples/03_kline/index_bars.py | 54 +- examples/03_kline/security_bars.py | 62 +- examples/04_minute/history_minute_data.py | 40 +- examples/04_minute/minute_time_data.py | 40 +- .../05_transaction/history_transaction.py | 42 +- examples/05_transaction/transaction_data.py | 42 +- examples/06_finance/company_info.py | 43 +- examples/06_finance/finance_info.py | 91 +- examples/06_finance/price_limits.py | 24 +- examples/06_finance/xdxr_info.py | 52 +- examples/07_block/block_info.py | 46 +- examples/08_fund_flow/fund_flow.py | 37 +- examples/08_fund_flow/history_fund_flow.py | 44 +- examples/09_file_download/report_file.py | 101 +- examples/10_offline/block_data.py | 62 +- examples/10_offline/daily_bars.py | 48 +- examples/10_offline/detect_home.py | 111 +- examples/10_offline/ex_daily_bars.py | 72 +- examples/10_offline/gbbq.py | 85 +- examples/10_offline/history_financial.py | 81 +- examples/10_offline/min_bars.py | 97 +- examples/11_mac_quotes/quotes_list.py | 81 ++ examples/11_mac_quotes/stock_quotes.py | 39 + examples/12_mac_kline/kline_offset.py | 23 + examples/12_mac_kline/stock_kline.py | 96 ++ examples/13_mac_tick/chart_sampling.py | 49 + examples/13_mac_tick/multi_day_tick.py | 46 + examples/13_mac_tick/tick_chart.py | 59 + examples/14_mac_transaction/transaction.py | 80 ++ examples/15_mac_board/belong_board.py | 42 + examples/15_mac_board/board_list.py | 70 + examples/15_mac_board/board_members.py | 61 + examples/16_mac_capital/capital_flow.py | 53 + examples/17_mac_monitor/auction.py | 41 + examples/17_mac_monitor/server_info.py | 29 + examples/17_mac_monitor/symbol_info.py | 41 + examples/17_mac_monitor/unusual.py | 49 + examples/18_mac_ex/ex_goods_list.py | 65 + examples/18_mac_ex/ex_kline.py | 72 + examples/18_mac_ex/ex_quotes.py | 42 + examples/18_mac_ex/ex_tick_chart.py | 61 + examples/19_unified/unified_client.py | 58 + examples/20_cli/cli_examples.sh | 315 +++++ pyproject.toml | 7 +- src/easy_tdx/__init__.py | 34 +- src/easy_tdx/cli/__init__.py | 59 + src/easy_tdx/cli/cmd_admin.py | 46 + src/easy_tdx/cli/cmd_auction.py | 30 + src/easy_tdx/cli/cmd_board.py | 106 ++ src/easy_tdx/cli/cmd_capital.py | 30 + src/easy_tdx/cli/cmd_ex.py | 177 +++ src/easy_tdx/cli/cmd_finance.py | 33 + src/easy_tdx/cli/cmd_info.py | 51 + src/easy_tdx/cli/cmd_kline.py | 54 + src/easy_tdx/cli/cmd_monitor.py | 58 + src/easy_tdx/cli/cmd_quote.py | 81 ++ src/easy_tdx/cli/cmd_tick.py | 44 + src/easy_tdx/cli/cmd_transaction.py | 43 + src/easy_tdx/cli/conn.py | 37 + src/easy_tdx/cli/output.py | 60 + src/easy_tdx/cli/parsers.py | 188 +++ src/easy_tdx/client.py | 175 ++- src/easy_tdx/codec/bitmap.py | 489 +++++++ src/easy_tdx/codec/mac_frame.py | 50 + src/easy_tdx/config.py | 280 ++++ src/easy_tdx/ex/__init__.py | 6 +- src/easy_tdx/ex/client.py | 26 +- .../ex/commands/get_instrument_count.py | 2 +- src/easy_tdx/ex/commands/login.py | 49 + src/easy_tdx/ex/mac_client.py | 715 ++++++++++ src/easy_tdx/ex/models.py | 35 +- src/easy_tdx/ex/transport/async_.py | 39 +- src/easy_tdx/ex/transport/sync.py | 78 +- src/easy_tdx/mac/__init__.py | 1 + src/easy_tdx/mac/client.py | 1257 +++++++++++++++++ src/easy_tdx/mac/commands/__init__.py | 33 + src/easy_tdx/mac/commands/board_list.py | 96 ++ .../mac/commands/board_members_quotes.py | 114 ++ src/easy_tdx/mac/commands/chart_sampling.py | 47 + src/easy_tdx/mac/commands/file_query.py | 90 ++ src/easy_tdx/mac/commands/goods_list.py | 77 + src/easy_tdx/mac/commands/kline_offset.py | 38 + src/easy_tdx/mac/commands/server_info.py | 74 + src/easy_tdx/mac/commands/symbol_auction.py | 66 + src/easy_tdx/mac/commands/symbol_bar.py | 122 ++ .../mac/commands/symbol_belong_board.py | 90 ++ .../mac/commands/symbol_capital_flow.py | 85 ++ src/easy_tdx/mac/commands/symbol_info.py | 88 ++ src/easy_tdx/mac/commands/symbol_quotes.py | 99 ++ .../mac/commands/symbol_tick_chart.py | 112 ++ .../mac/commands/symbol_transaction.py | 76 + src/easy_tdx/mac/commands/tick_charts.py | 129 ++ src/easy_tdx/mac/commands/unusual.py | 173 +++ src/easy_tdx/mac/enums.py | 220 +++ src/easy_tdx/mac/models.py | 216 +++ src/easy_tdx/transport/async_.py | 17 +- src/easy_tdx/transport/sync.py | 203 +-- src/easy_tdx/unified.py | 589 ++++++++ tests/unit/test_async_transport.py | 2 +- 108 files changed, 10345 insertions(+), 932 deletions(-) create mode 100644 examples/11_mac_quotes/quotes_list.py create mode 100644 examples/11_mac_quotes/stock_quotes.py create mode 100644 examples/12_mac_kline/kline_offset.py create mode 100644 examples/12_mac_kline/stock_kline.py create mode 100644 examples/13_mac_tick/chart_sampling.py create mode 100644 examples/13_mac_tick/multi_day_tick.py create mode 100644 examples/13_mac_tick/tick_chart.py create mode 100644 examples/14_mac_transaction/transaction.py create mode 100644 examples/15_mac_board/belong_board.py create mode 100644 examples/15_mac_board/board_list.py create mode 100644 examples/15_mac_board/board_members.py create mode 100644 examples/16_mac_capital/capital_flow.py create mode 100644 examples/17_mac_monitor/auction.py create mode 100644 examples/17_mac_monitor/server_info.py create mode 100644 examples/17_mac_monitor/symbol_info.py create mode 100644 examples/17_mac_monitor/unusual.py create mode 100644 examples/18_mac_ex/ex_goods_list.py create mode 100644 examples/18_mac_ex/ex_kline.py create mode 100644 examples/18_mac_ex/ex_quotes.py create mode 100644 examples/18_mac_ex/ex_tick_chart.py create mode 100644 examples/19_unified/unified_client.py create mode 100644 examples/20_cli/cli_examples.sh create mode 100644 src/easy_tdx/cli/__init__.py create mode 100644 src/easy_tdx/cli/cmd_admin.py create mode 100644 src/easy_tdx/cli/cmd_auction.py create mode 100644 src/easy_tdx/cli/cmd_board.py create mode 100644 src/easy_tdx/cli/cmd_capital.py create mode 100644 src/easy_tdx/cli/cmd_ex.py create mode 100644 src/easy_tdx/cli/cmd_finance.py create mode 100644 src/easy_tdx/cli/cmd_info.py create mode 100644 src/easy_tdx/cli/cmd_kline.py create mode 100644 src/easy_tdx/cli/cmd_monitor.py create mode 100644 src/easy_tdx/cli/cmd_quote.py create mode 100644 src/easy_tdx/cli/cmd_tick.py create mode 100644 src/easy_tdx/cli/cmd_transaction.py create mode 100644 src/easy_tdx/cli/conn.py create mode 100644 src/easy_tdx/cli/output.py create mode 100644 src/easy_tdx/cli/parsers.py create mode 100644 src/easy_tdx/codec/bitmap.py create mode 100644 src/easy_tdx/codec/mac_frame.py create mode 100644 src/easy_tdx/config.py create mode 100644 src/easy_tdx/ex/commands/login.py create mode 100644 src/easy_tdx/ex/mac_client.py create mode 100644 src/easy_tdx/mac/__init__.py create mode 100644 src/easy_tdx/mac/client.py create mode 100644 src/easy_tdx/mac/commands/__init__.py create mode 100644 src/easy_tdx/mac/commands/board_list.py create mode 100644 src/easy_tdx/mac/commands/board_members_quotes.py create mode 100644 src/easy_tdx/mac/commands/chart_sampling.py create mode 100644 src/easy_tdx/mac/commands/file_query.py create mode 100644 src/easy_tdx/mac/commands/goods_list.py create mode 100644 src/easy_tdx/mac/commands/kline_offset.py create mode 100644 src/easy_tdx/mac/commands/server_info.py create mode 100644 src/easy_tdx/mac/commands/symbol_auction.py create mode 100644 src/easy_tdx/mac/commands/symbol_bar.py create mode 100644 src/easy_tdx/mac/commands/symbol_belong_board.py create mode 100644 src/easy_tdx/mac/commands/symbol_capital_flow.py create mode 100644 src/easy_tdx/mac/commands/symbol_info.py create mode 100644 src/easy_tdx/mac/commands/symbol_quotes.py create mode 100644 src/easy_tdx/mac/commands/symbol_tick_chart.py create mode 100644 src/easy_tdx/mac/commands/symbol_transaction.py create mode 100644 src/easy_tdx/mac/commands/tick_charts.py create mode 100644 src/easy_tdx/mac/commands/unusual.py create mode 100644 src/easy_tdx/mac/enums.py create mode 100644 src/easy_tdx/mac/models.py create mode 100644 src/easy_tdx/unified.py diff --git a/README.md b/README.md index 0f2b079..609feb2 100644 --- a/README.md +++ b/README.md @@ -1,634 +1,482 @@ # easy-tdx [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![PyPI](https://img.shields.io/pypi/v/easy-tdx.svg)](https://pypi.org/project/easy-tdx/) -通达信(tdx)是中国使用最广泛的券商行情终端之一,其私有 TCP 协议长期缺乏官方 SDK。[pytdx](https://github.com/rainx/pytdx) 率先完成了协议逆向与离线数据读取,为整个生态奠定了基础;[mootdx](https://github.com/mootdx/mootdx) 在此之上做了工程化封装,让更多开发者得以使用;[xmtdx](https://github.com/minionszyw/xmtdx) 进一步探索了现代 Python 接口设计。 - -easy-tdx 站在这些项目的肩膀上,从协议层重新实现:LEB128 价格编解码、自定义浮点成交量、帧解压缩与握手——每一层都有对应的离线 fixture 测试。commands 层不含 IO,与 transport 完全解耦;同步 + asyncio 双接口;strict mypy 通过;零运行时依赖;每条记录保留原始字节。覆盖标准行情、扩展市场(期货/港股/外盘)、离线本地数据读取、专业财务数据全场景。 - -感谢 rainx、mootdx 社区及 minionszyw 的开创性工作——没有他们,就不会有这个项目。 - -详见 [NOTICE](NOTICE) 和 [LICENSE](LICENSE) 文件。 - -## 特性 - -- **零依赖**:纯标准库,Python >= 3.10 -- **同步 + asyncio 双接口**:`TdxClient` / `AsyncTdxClient`,commands 层不含任何 IO -- **完整类型注解**:strict `mypy` + `ruff` 通过 -- **高可用传输**:同步/异步均支持 `ping_all()`、`from_best_host()`、断线自动重连 -- **修复 pytdx 已知 bug**(见下文) -- **保留原始字节**:每条数据记录含 `_raw: bytes`,未知字段以 `unknown_N` 命名而非丢弃 -- **保活心跳机制**:`AsyncTdxClient` 自动发送心跳包,确保长连接生产环境稳定性 -- **扩展行情**:`ExTdxClient` / `AsyncExTdxClient` 支持期货、港股、外盘等扩展市场(端口 7727) -- **离线数据读取**:从本地通达信安装目录直接读取日线、分钟线、财务、板块、股本变迁等数据,无需网络 -- **专业财务数据**:通过计算服务器下载历史财报 ZIP 文件并解析 +通达信 TCP 行情协议客户端。支持 A 股、港股、美股、期货全市场;内置 `easy-tdx` CLI 工具,默认 JSON 输出,天然适配 Claude Code、OpenClaw、Hermes 等 AI Agent 工具链。提供同步 + asyncio 双接口;strict mypy 通过;每一层编解码都有离线 fixture 测试覆盖。 ## 安装 ```bash -pip install -e . # 开发模式 -pip install -e ".[dev]" # 含测试/类型检查工具 -pip install -e ".[pandas]" # 含 pandas(可选) +pip install easy-tdx ``` -## 快速开始 +安装后自动注册 `easy-tdx` CLI 命令: -### 连接与基本查询 +```bash +easy-tdx --help +``` + +开发模式: + +```bash +pip install -e ".[dev]" +``` + +## CLI 参考 + +`easy-tdx` 默认输出 JSON(一行一条记录),`--table` 切换表格,`--output csv` 输出 CSV。 + +### 基础 + +```bash +easy-tdx ping # 服务器测速 +easy-tdx version # 版本号 +``` + +### 行情 + +```bash +# K 线 +easy-tdx kline SZ 000001 --count 30 --table +easy-tdx kline SH 600519 --period 5MIN --adjust QFQ + +# 实时报价 +easy-tdx quote "SZ 000001,SH 600519" --table + +# 市场分类报价(按涨幅排序) +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 +``` + +### 分时 / 成交 + +```bash +easy-tdx tick SZ 000001 --table +easy-tdx tick SH 600519 --days 5 +easy-tdx tick SZ 000001 --date 20250115 + +easy-tdx transaction SZ 000001 --count 100 --table +easy-tdx transaction SH 600519 --date 20250115 +``` + +### 板块 + +```bash +easy-tdx board-list --type GN --table +easy-tdx board-list --type HY --count 200 +easy-tdx board-members 881001 --table +easy-tdx belong-board SZ 000001 --table +``` + +### 资金 / 监控 + +```bash +easy-tdx capital-flow SH 600519 --table +easy-tdx auction SZ 000001 --table +easy-tdx unusual SH --count 100 --table +easy-tdx market-stat --table +easy-tdx server-info --table +easy-tdx symbol-info SZ 000001 --table +``` + +### 财务 + +```bash +easy-tdx f10 SH 600519 # F10 公司信息 +easy-tdx fund-flow SH 600519 # 历史资金流向 +``` + +### 扩展市场(港股/美股/期货) + +```bash +easy-tdx ex markets # 列出可用市场 +easy-tdx ex kline HK_MAIN_BOARD 00700 --count 30 --table # 港股 K 线 +easy-tdx ex kline US_STOCK AAPL --table # 美股 K 线 +easy-tdx ex quote US_STOCK TSLA --table # 美股报价 +easy-tdx ex quote-list HK_MAIN_BOARD --table # 港股商品列表 +easy-tdx ex tick HK_MAIN_BOARD 00700 --table # 港股分时 +``` + +## CLI 命令汇总 + +| 命令 | 说明 | +|------|------| +| `ping` | 服务器延迟测速 | +| `version` | 版本号 | +| `kline` | K 线(日/周/月/分钟,支持复权) | +| `quote` | 实时报价(单只/批量) | +| `quote-list` | 市场分类排序报价(A/SH/SZ/KCB/CYB) | +| `tick` | 分时图(单日/多日/历史) | +| `transaction` | 逐笔成交 | +| `board-list` | 板块列表(行业/概念/风格) | +| `board-members` | 板块成分股报价 | +| `belong-board` | 个股所属板块 | +| `capital-flow` | 资金流向 | +| `auction` | 集合竞价 | +| `unusual` | 市场异动 | +| `market-stat` | 全市场涨跌统计 | +| `server-info` | 服务器交易时段 | +| `symbol-info` | 个股特征快照 | +| `f10` | F10 公司信息 | +| `fund-flow` | 历史资金流向 | +| `ex kline` | 扩展市场 K 线 | +| `ex quote` | 扩展市场报价 | +| `ex quote-list` | 扩展市场商品列表 | +| `ex tick` | 扩展市场分时 | +| `ex markets` | 列出可用扩展市场 | + +## Python API + +### 连接管理 + +所有客户端支持 `from_best_host()` 自动选最低延迟服务器: + +```python +from easy_tdx import MacClient + +with MacClient.from_best_host() as c: + df = c.get_stock_kline(...) +``` + +| 客户端 | 端口 | 覆盖范围 | +|--------|------|----------| +| `MacClient` / `AsyncMacClient` | 7709 | A 股行情(MAC 协议,推荐) | +| `MacExClient` / `AsyncMacExClient` | 7727 | 港股/美股/期货(MAC 协议) | +| `UnifiedTdxClient` / `AsyncUnifiedTdxClient` | 自动 | A 股 + 扩展市场统一入口 | +| `TdxClient` / `AsyncTdxClient` | 7709 | A 股行情(标准协议) | + +### MAC 协议(推荐) + +#### 报价 + +```python +from easy_tdx import MacClient, Market, Category, SortType, SortOrder + +with MacClient.from_best_host() as c: + # 批量报价(最多 80 只/次) + df = c.get_stock_quotes([(Market.SH, "600519"), (Market.SZ, "000858")]) + + # 市场分类排序报价 + df = c.get_stock_quotes_list( + Category.A, count=20, + sort_type=SortType.CHANGE_PCT, + sort_order=SortOrder.DESC, + ) +``` + +返回列:`market, code, name` + 动态字段(`pre_close, open, high, low, close, vol, amount, turnover, vol_ratio` 等)。 + +#### K 线(支持复权) + +```python +from easy_tdx import MacClient, Market, Period, Adjust + +with MacClient.from_best_host() as c: + # 日K前复权 + df = c.get_stock_kline(Market.SH, "600519", Period.DAILY, count=10, adjust=Adjust.QFQ) + # 5分钟线 + df = c.get_stock_kline(Market.SZ, "000001", Period.MIN_5, count=100) +``` + +返回列:`datetime, open, close, high, low, vol, amount`。 + +#### 分时 + +```python +with MacClient.from_best_host() as c: + df = c.get_tick_chart(Market.SH, "600519") # 单日分时 + df = c.get_tick_charts(Market.SH, "600519", days=3) # 多日分时(最多5天) + df = c.get_chart_sampling(Market.SH, "600519") # 240点缩略采样 +``` + +#### 逐笔成交 + +```python +with MacClient.from_best_host() as c: + df = c.get_transactions(Market.SH, "600519", count=100) + df = c.get_transactions(Market.SH, "600519", count=100, date=20250115) +``` + +#### 板块 + +```python +from easy_tdx import BoardType + +with MacClient.from_best_host() as c: + df = c.get_board_list(BoardType.GN) # 概念板块 + df = c.get_board_members("881001", sort_type=SortType.CHANGE_PCT) + df = c.get_belong_board(Market.SZ, "000001") # 个股所属板块 +``` + +#### 资金流向 + +```python +with MacClient.from_best_host() as c: + df = c.get_capital_flow(Market.SH, "600519") +``` + +返回列:`date, main_in, main_out, main_net, small_in/out/net, mid_in/out/net, large_in/out/net`。 + +#### 监控 + +```python +with MacClient.from_best_host() as c: + df = c.get_auction(Market.SH, "600519") # 集合竞价 + df = c.get_unusual(Market.SH) # 市场异动 + df = c.get_symbol_info(Market.SZ, "000001") # 个股特征快照 + df = c.get_server_info() # 服务器交易时段 +``` + +### 扩展市场 + +```python +from easy_tdx import MacExClient, ExMarket, Period + +with MacExClient.from_best_host() as c: + count = c.goods_count(ExMarket.HK_MAIN_BOARD) + df = c.goods_list(ExMarket.HK_MAIN_BOARD, start=0, count=50) + df = c.goods_kline(ExMarket.US_STOCK, "AAPL", Period.DAILY, count=10) + df = c.goods_quotes([(ExMarket.HK_MAIN_BOARD, "00700")]) + df = c.goods_tick_chart(ExMarket.HK_MAIN_BOARD, "00700") + df = c.goods_transaction(ExMarket.HK_MAIN_BOARD, "00700", count=100) +``` + +### 统一客户端 + +```python +from easy_tdx import UnifiedTdxClient, ExMarket, Market, Period + +with UnifiedTdxClient() as client: + # A 股 -- 自动路由到 MacClient + df = client.get_stock_kline(Market.SH, "600519", Period.DAILY, count=5) + df = client.get_stock_quotes([(Market.SH, "600519")]) + df = client.get_board_list() + + # 扩展市场 -- 自动路由到 MacExClient + df = client.goods_kline(ExMarket.HK_MAIN_BOARD, "00700", Period.DAILY, count=5) +``` + +### 标准协议 ```python from easy_tdx import TdxClient, Market, KlineCategory -# 手动指定服务器 -with TdxClient("180.153.18.170") as c: - count = c.get_security_count(Market.SH) - print(f"沪市证券总数: {count}") - -# 自动优选最低延迟服务器 with TdxClient.from_best_host() as c: - df = c.get_security_bars(Market.SH, "600000", KlineCategory.DAY, 0, 5) - print(df.to_string(index=False)) - # date open close high low vol amount - # 2025-01-02 10.25 10.12 10.25 10.08 108154752.0 1.078280e+09 -``` - -### asyncio - -```python -import asyncio -from easy_tdx import AsyncTdxClient, Market, KlineCategory - -async def main(): - async with AsyncTdxClient.from_best_host() as c: - df = await c.get_security_bars( - Market.SH, "600000", KlineCategory.DAY, 0, 5 - ) - print(df.to_string(index=False)) - -asyncio.run(main()) -``` - -### 服务器测速 - -```python -from easy_tdx import TdxClient - -# 测速并排序 -results = TdxClient.ping_all() -for host, latency in results: - print(f"{host} {latency * 1000:.0f} ms") -``` - -## API 参考 - -### 连接管理 - -| 方法 | 说明 | -|------|------| -| `TdxClient(host, port=7709, timeout=15.0)` | 指定服务器创建客户端 | -| `TdxClient.from_best_host(ping_timeout=5.0)` | 自动选延迟最低的服务器 | -| `TdxClient.ping_all(timeout=5.0)` | 并发测速,返回 `[(host, seconds), ...]` | -| `AsyncTdxClient` / `AsyncTdxClient.from_best_host()` | 异步版,接口一一对应 | - -内置服务器列表 `KNOWN_HOSTS`(8 台)和计算服务器 `CALC_HOSTS`(1 台)。 - -### 市场信息 - -```python -with TdxClient.from_best_host() as c: - # 市场证券总数 count = c.get_security_count(Market.SH) - - # 证券列表(分页,每页约 1000 条) stocks = c.get_security_list(Market.SH, start=0) - # stocks[0].code / .name / .pre_close / .industry_tdx / .industry_sw - - # 沪深 A 股完整列表(自动挂载行业信息,本地缓存 1 天) - all_stocks = c.get_security_list_all() - - # 批量实时五档行情(最多 80 只/次) - quotes = c.get_security_quotes([ - (Market.SH, "600000"), # 浦发银行 - (Market.SH, "600519"), # 贵州茅台 - (Market.SZ, "000001"), # 平安银行 - (Market.SZ, "000858"), # 五粮液 - ]) - # quotes[0].price / .pre_close / .open / .high / .low / .bid1..bid5 / .ask1..ask5 - - # 全市场涨跌统计 - stat = c.get_market_stat() - # stat.up_count / .down_count / .neutral_count / .total_amount / .total_market_cap -``` - -### K 线数据 - -```python -from easy_tdx import Market, KlineCategory - -with TdxClient.from_best_host() as c: - # 个股 K 线 + quotes = c.get_security_quotes([(Market.SH, "600000"), (Market.SZ, "000001")]) bars = c.get_security_bars(Market.SZ, "002176", KlineCategory.DAY, 0, 100) - - # 指数 K 线(常用指数代码:上证 "999999",深成 "399001",创业板 "399006") - bars = c.get_index_bars(Market.SH, "999999", KlineCategory.DAY, 0, 10) -``` - -K 线类别: - -``` -KlineCategory.MIN_1 MIN_5 MIN_15 MIN_30 MIN_60 -KlineCategory.DAY WEEK MONTH YEAR -``` - -K 线字段:`date`(日线及以上)或 `datetime`(分钟线) `open` `close` `high` `low` `vol` `amount` - -### 分时数据 - -```python -with TdxClient.from_best_host() as c: - # 今日分时(240 条) - bars = c.get_minute_time_data(Market.SH, "600000") - - # 历史某日分时,date 为 YYYYMMDD 格式整数 - bars = c.get_history_minute_time_data(Market.SH, "600000", 20250110) -``` - -分时字段:`datetime` `price` `vol` - -### 逐笔成交 - -```python -with TdxClient.from_best_host() as c: - # 当日逐笔成交(分页) - records = c.get_transaction_data(Market.SH, "600000", 0, 20) - - # 历史逐笔成交 - records = c.get_history_transaction_data(Market.SH, "600000", 20250110, 0, 20) -``` - -成交字段:`datetime` `price` `vol` `buyorsell`(0=买, 1=卖, 2=中性, 8=集合竞价) - -### 财务与公司信息 - -```python -from easy_tdx import XDXR_CATEGORY_NAMES - -with TdxClient.from_best_host() as c: - # 除权除息历史 - records = c.get_xdxr_info(Market.SH, "600519") - # records[0].fenhong / .songzhuangu / .peigujia / .peigu - - # 最新财务数据 - info = c.get_finance_info(Market.SH, "600519") - # info.zong_guben / .liutong_guben / .jing_lirun / .zhuying_shouru / ... - - # 涨跌停价计算 - quotes = c.get_security_quotes([(Market.SH, "600519")]) - limit_up, limit_down = c.get_price_limits( - Market.SH, "600519", "贵州茅台", quotes[0].pre_close - ) - - # 公司信息目录 - categories = c.get_company_info_category(Market.SH, "600519") - for cat in categories: - print(cat.name, cat.filename, cat.start, cat.length) - - # 公司信息内容 - content = c.get_company_info_content( - Market.SH, "600519", cat.filename, cat.start, cat.length - ) -``` - -### 板块信息 - -```python -with TdxClient.from_best_host() as c: - # 行业/指数板块 - blocks = c.get_block_info("block_zs.dat") - # 概念板块 - blocks = c.get_block_info("block_gn.dat") - # 风格板块 - blocks = c.get_block_info("block_fg.dat") - # blocks[0].name / .category / .count / .codes -``` - -### 资金流向 - -```python -with TdxClient.from_best_host() as c: - # 当日资金流向(超大/大/中/小单) + minute = c.get_minute_time_data(Market.SH, "600000") + trades = c.get_transaction_data(Market.SH, "600000", 0, 20) flow = c.get_fund_flow(Market.SH, "600519") - # flow.super_in / .super_out / .large_in / .large_out / .main_net_inflow - - # 历史日线资金流向序列 - flows = c.get_history_fund_flow(Market.SH, "600519", 0, 10) - # flows[0].date / .super_in / .main_net_inflow + blocks = c.get_block_info("block_gn.dat") + xdxr = c.get_xdxr_info(Market.SH, "600519") + stat = c.get_market_stat() ``` -### 文件下载 +`AsyncTdxClient` 提供对应的 `async def` 方法,接口一一对应。 -```python -from easy_tdx import CALC_HOSTS +### 离线数据读取 -with TdxClient.from_best_host() as c: - # 行情服务器可用的文件 - data = c.get_report_file("tdxhy.cfg") # 行业映射配置 - data = c.get_report_file("block_gn.dat") # 概念板块 - - # 计算服务器:专业财务数据 - with TdxClient(CALC_HOSTS[0]) as calc: - file_list = calc.get_financial_file_list() - # file_list[0].filename / .filesize / .hash - - zip_data = calc.get_financial_file("tdxfin/gpcw20260331.zip") - records = calc.get_financial_records("tdxfin/gpcw20260331.zip") - # records[0].market / .code / .report_date / .fields -``` - -### 扩展行情(期货、港股、外盘) - -```python -from easy_tdx import ExTdxClient - -# 扩展行情服务器端口 7727 -with ExTdxClient() as c: - markets = c.get_markets() # 可用市场列表 - count = c.get_instrument_count() # 品种总数 - instruments = c.get_instrument_info(0, 50) # 品种信息(分页) - quote = c.get_instrument_quote(market, code) # 单品种行情 - - # K 线(支持日期范围查询) - bars = c.get_instrument_bars(market, code, category, start, count) - bars = c.get_history_instrument_bars_range(market, code, date_start, date_end) - - # 分时 / 逐笔 - minute = c.get_minute_time_data(market, code) - trades = c.get_transaction_data(market, code, start, count) -``` - -`AsyncExTdxClient` 提供与同步版对应的 `async def` 方法。 - -## 离线数据读取 - -从本地通达信安装目录直接读取数据文件,无需网络连接。离线模块的路径检测优先级: - -1. `TDX_HOME` 环境变量 -2. 平台常见路径猜测(Windows: `C:\new_jyplug`、`C:\new_tdx` 等) - -```python -# Windows -set TDX_HOME=C:\new_jyplug - -# Linux/macOS -export TDX_HOME=/opt/new_tdx -``` - -### 日线 K 线 +无需网络,从本地通达信安装目录直接读取: ```python from easy_tdx.offline import detect_tdx_home, read_daily_bars, find_daily_bar_file from easy_tdx import Market home = detect_tdx_home() - -# 通过 市场+代码 自动定位文件 filepath = find_daily_bar_file(Market.SH, "600000") bars = read_daily_bars(filepath) - -for bar in bars[-10:]: - print(f"{bar.year}-{bar.month:02d}-{bar.day:02d} " - f"开:{bar.open:.2f} 收:{bar.close:.2f} 量:{bar.vol:.0f}") ``` -文件位于 `vipdoc/{sh,sz}/lday/`,如 `sh600000.day`。自动识别证券类型(A 股/B 股/指数/基金/债券)并应用对应的价格和成交量系数。 +支持:日线、分钟线、扩展市场日线、板块、股本变迁、历史财务数据。 -### 分钟 K 线 +## 枚举参考 -```python -from easy_tdx.offline import ( - read_5min_bars, read_lc_min_bars, - find_5min_bar_file, find_lc1_bar_file, find_lc5_bar_file, -) -from easy_tdx import Market +### Period(K 线周期) -# .5 文件(OHLC 为整数 / 100) -filepath = find_5min_bar_file(Market.SH, "600000") -bars = read_5min_bars(filepath) +| 值 | 名称 | 说明 | +|----|------|------| +| 7 | `MIN_1` | 1 分钟 | +| 0 | `MIN_5` | 5 分钟 | +| 1 | `MIN_15` | 15 分钟 | +| 2 | `MIN_30` | 30 分钟 | +| 3 | `MIN_60` | 60 分钟 | +| 4 | `DAILY` | 日线 | +| 5 | `WEEKLY` | 周线 | +| 6 | `MONTHLY` | 月线 | +| 10 | `QUARTERLY` | 季线 | +| 11 | `YEARLY` | 年线 | -# .lc1 文件(1 分钟线,OHLC 为浮点数) -filepath = find_lc1_bar_file(Market.SH, "600000") -bars = read_lc_min_bars(filepath) +### Adjust(复权类型) -# .lc5 文件(5 分钟线,OHLC 为浮点数) -filepath = find_lc5_bar_file(Market.SZ, "002176") -bars = read_lc_min_bars(filepath) -``` +| 值 | 名称 | 说明 | +|----|------|------| +| 0 | `NONE` | 不复权 | +| 1 | `QFQ` | 前复权 | +| 2 | `HFQ` | 后复权 | -文件位于 `vipdoc/{sh,sz}/fzline/`,如 `sh600000.5`、`sh600000.lc1`、`sh600000.lc5`。 +### Category(市场分类) -### 扩展市场日线 +| 值 | 名称 | 说明 | +|----|------|------| +| 0 | `SH` | 上证 A 股 | +| 2 | `SZ` | 深证 A 股 | +| 6 | `A` | 全部 A 股 | +| 7 | `B` | B 股 | +| 8 | `KCB` | 科创板 | +| 12 | `BJ` | 北证 A 股 | +| 14 | `CYB` | 创业板 | -```python -from easy_tdx.offline import read_ex_daily_bars +### BoardType(板块类型) -# 期货、港股、外盘等扩展市场数据 -# 文件位于 vipdoc/ds/lday/,如 29#A1801.day -bars = read_ex_daily_bars(r"C:\new_jyplug\vipdoc\ds\lday\38#2_CPI.day") -# bar.open / .high / .low / .close / .settlement / .vol -``` +| 值 | 名称 | 说明 | +|----|------|------| +| 0 | `HY` | 行业一级 | +| 1 | `HY2` | 行业二级 | +| 3 | `GN` | 概念 | +| 4 | `FG` | 风格 | +| 5 | `DQ` | 地区 | +| 255 | `ALL` | 全部 | -### 板块数据 +### SortType(排序字段) -```python -from easy_tdx.offline import read_block_dat, read_customer_blocks +| 名称 | 说明 | +|------|------| +| `CODE` | 代码 | +| `PRICE` | 现价 | +| `CHANGE_PCT` | 涨幅% | +| `VOLUME` | 成交量 | +| `TOTAL_AMOUNT` | 成交额 | +| `TURNOVER_RATE` | 换手% | +| `MAIN_NET_AMOUNT` | 主力净额 | -# 系统板块(本地 .dat 文件) -blocks = read_block_dat(r"C:\new_jyplug\vipdoc\block_zs.dat") -# blocks[0].name / .category / .count / .codes +### ExMarket(扩展市场) -# 自定义板块(blocknew 目录) -blocks = read_customer_blocks(r"C:\new_jyplug\T0002\blocknew") -# blocks[0].blockname / .codes -``` +| 值 | 名称 | 说明 | +|----|------|------| +| 28 | `ZZ_FUTURES` | 郑州商品 | +| 29 | `DL_FUTURES` | 大连商品 | +| 30 | `SH_FUTURES` | 上海期货 | +| 31 | `HK_MAIN_BOARD` | 香港主板 | +| 47 | `CFFEX_FUTURES` | 中金所期货 | +| 48 | `HK_GEM` | 香港创业板 | +| 74 | `US_STOCK` | 美国股票 | -支持本地 .dat 文件离线读取,本地不存在时可通过 `TdxClient.get_block_info()` 在线获取。 +### Market(市场) -### 股本变迁 - -```python -from easy_tdx.offline import read_gbbq - -records = read_gbbq(r"C:\new_jyplug\T0002\hq_cache\gbbq") -# records[0].market / .code / .datetime / .category / .hongli_panqianliutong / ... -``` - -gbbq 文件使用 XOR 加密存储,读取时自动解密。 - -### 历史财务数据 - -```python -from easy_tdx.offline import read_history_financial - -# 支持 .dat 和 .zip 文件(.zip 自动解压) -records = read_history_financial(r"C:\new_jyplug\vipdoc\fin\gpcw20260331.zip") -# records[0].code / .market / .report_date / .fields -``` - -文件可通过 `TdxClient.get_financial_file_list()` 查询可用文件,再用 `get_financial_file()` 下载到本地。 - -### 路径检测 - -```python -from easy_tdx.offline import detect_tdx_home, resolve_vipdoc - -# 自动检测通达信安装目录 -home = detect_tdx_home() - -# 解析 vipdoc 数据目录 -vipdoc = resolve_vipdoc() -``` - -vipdoc 目录结构: - -``` -vipdoc/ -├── sh/lday/ 上海日线 sh600000.day -├── sh/fzline/ 上海分钟线 sh600000.5 / .lc1 / .lc5 -├── sz/lday/ 深圳日线 sz000001.day -├── sz/fzline/ 深圳分钟线 sz000001.5 / .lc1 / .lc5 -├── ds/lday/ 扩展市场 29#A1801.day -└── fin/ 历史财务 gpcw*.dat / gpcw*.zip -``` +| 值 | 名称 | 说明 | +|----|------|------| +| 0 | `SZ` | 深圳 | +| 1 | `SH` | 上海 | +| 2 | `BJ` | 北京 | ## 完整 API 列表 +### MacClient / AsyncMacClient + +| 方法 | 说明 | +|------|------| +| `get_stock_quotes(stocks, fields)` | 批量实时报价 | +| `get_stock_quotes_list(category, ...)` | 市场分类排序报价 | +| `get_stock_kline(market, code, period, ...)` | K 线(支持复权) | +| `get_tick_chart(market, code, date)` | 单日分时图 | +| `get_tick_charts(market, code, days)` | 多日分时图 | +| `get_chart_sampling(market, code)` | 分时缩略采样 | +| `get_transactions(market, code, ...)` | 逐笔成交 | +| `get_symbol_info(market, code)` | 个股特征快照 | +| `get_board_list(board_type, ...)` | 板块列表 | +| `get_board_members(board_symbol, ...)` | 板块成分股报价 | +| `get_belong_board(market, code)` | 个股所属板块 | +| `get_capital_flow(market, code)` | 资金流向 | +| `get_auction(market, code)` | 集合竞价 | +| `get_unusual(market, ...)` | 市场异动 | +| `get_server_info()` | 服务器交易时段 | +| `get_kline_offset(offset, count)` | K 线偏移信息 | +| `get_goods_list(market, ...)` | 扩展市场商品列表 | + +### MacExClient / AsyncMacExClient + +| 方法 | 说明 | +|------|------| +| `goods_count(market)` | 商品总数 | +| `goods_list(market, start, count)` | 商品列表 | +| `goods_quotes(stocks, fields)` | 批量报价 | +| `goods_quotes_list(market, ...)` | 市场分类报价列表 | +| `goods_kline(market, code, period, ...)` | K 线(支持复权) | +| `goods_tick_chart(market, code, ...)` | 分时图 | +| `goods_chart_sampling(market, code)` | 分时缩略采样 | +| `goods_transaction(market, code, ...)` | 逐笔成交 | + ### TdxClient / AsyncTdxClient | 方法 | 说明 | |------|------| | `get_security_count(market)` | 市场证券总数 | -| `get_security_list(market, start)` | 证券列表(每页约 1000 条) | -| `get_security_list_all()` | 沪深 A 股完整列表(含行业映射,本地缓存 1 天) | -| `get_security_quotes([(market, code), ...])` | 批量实时五档行情(最多 80 只/次) | -| `get_price_limits(market, code, name, pre_close)` | 计算涨跌停价 | -| `get_security_bars(market, code, category, start, count)` | 个股 K 线 | -| `get_index_bars(market, code, category, start, count)` | 指数 K 线 | -| `get_minute_time_data(market, code)` | 今日分时(240 条) | +| `get_security_list(market, start)` | 证券列表(分页) | +| `get_security_list_all()` | 沪深 A 股完整列表(含行业) | +| `get_security_quotes(stocks)` | 批量五档行情 | +| `get_security_bars(market, code, ...)` | 个股 K 线 | +| `get_index_bars(market, code, ...)` | 指数 K 线 | +| `get_minute_time_data(market, code)` | 今日分时 | | `get_history_minute_time_data(market, code, date)` | 历史分时 | -| `get_transaction_data(market, code, start, count)` | 当日逐笔成交 | -| `get_history_transaction_data(market, code, date, start, count)` | 历史逐笔成交 | +| `get_transaction_data(market, code, ...)` | 当日逐笔成交 | +| `get_history_transaction_data(...)` | 历史逐笔成交 | | `get_fund_flow(market, code)` | 当日资金流向 | -| `get_history_fund_flow(market, code, start, count)` | 历史日线资金流向 | +| `get_history_fund_flow(market, code, ...)` | 历史资金流向 | | `get_xdxr_info(market, code)` | 除权除息历史 | | `get_finance_info(market, code)` | 最新财务数据 | | `get_company_info_category(market, code)` | 公司信息目录 | -| `get_company_info_content(market, code, filename, offset, length)` | 公司信息文本 | +| `get_company_info_content(...)` | 公司信息文本 | | `get_block_info(filename)` | 板块信息 | | `get_report_file(filename)` | 下载服务器文件 | | `get_market_stat()` | 全市场涨跌统计 | -| `get_financial_file_list()` | 计算服务器财务文件列表 | -| `get_financial_file(filename)` | 下载财务文件 | -| `get_financial_records(filename)` | 下载并解析财务记录 | - -### ExTdxClient / AsyncExTdxClient - -| 方法 | 说明 | -|------|------| -| `get_markets()` | 可用市场列表 | -| `get_instrument_count()` | 品种总数 | -| `get_instrument_info(start, count)` | 品种信息(分页) | -| `get_instrument_quote(market, code)` | 单品种行情 | -| `get_instrument_quote_list(market, start, count)` | 批量行情 | -| `get_instrument_bars(market, code, category, start, count)` | 品种 K 线 | -| `get_history_instrument_bars_range(market, code, start, end)` | 日期范围 K 线 | -| `get_minute_time_data(market, code)` | 分时数据 | -| `get_history_minute_time_data(market, code, date)` | 历史分时 | -| `get_transaction_data(market, code, start, count)` | 逐笔成交 | -| `get_history_transaction_data(market, code, date, start, count)` | 历史逐笔 | - -### easy_tdx.offline - -| 函数 | 说明 | -|------|------| -| `detect_tdx_home()` | 检测通达信安装目录 | -| `resolve_vipdoc(path)` | 解析 vipdoc 数据目录 | -| `read_daily_bars(filepath)` | 读取日线 .day 文件 | -| `find_daily_bar_file(market, code)` | 定位日线文件路径 | -| `read_5min_bars(filepath)` | 读取 .5 分钟线文件 | -| `read_lc_min_bars(filepath)` | 读取 .lc1/.lc5 分钟线文件 | -| `find_5min_bar_file(market, code)` | 定位 .5 文件路径 | -| `find_lc1_bar_file(market, code)` | 定位 .lc1 文件路径 | -| `find_lc5_bar_file(market, code)` | 定位 .lc5 文件路径 | -| `read_ex_daily_bars(filepath)` | 读取扩展市场日线 | -| `read_block_dat(filepath)` | 读取系统板块 .dat 文件 | -| `read_customer_blocks(block_dir)` | 读取自定义板块目录 | -| `read_gbbq(filepath)` | 读取股本变迁文件 | -| `read_history_financial(filepath)` | 读取历史财务数据 | - -## 数据模型 - -所有 dataclass 字段均有类型注解。每条记录附带 `_raw: bytes`(原始协议字节)。 - -### SecurityBar(K 线) - -``` -date(日线及以上)或 datetime(分钟线) -open close high low vol amount -``` - -### SecurityQuote(实时行情) - -``` -market code price pre_close open high low -vol cur_vol amount s_vol b_vol -bid1..bid5 bid_vol1..bid_vol5 -ask1..ask5 ask_vol1..ask_vol5 -server_time -_raw -``` - -`limit_up` / `limit_down` 默认为 `None`,涨跌停价应通过 `get_price_limits()` 计算。 - -### SecurityInfo(证券列表) - -``` -market code name volunit decimal_point pre_close -industry_tdx industry_sw -``` - -### MinuteBar(分时) - -``` -datetime price vol -``` - -### TransactionRecord(逐笔成交) - -``` -datetime price vol buyorsell -``` - -### XdxrRecord(除权除息) - -``` -date market code category name -fenhong peigujia songzhuangu peigu suogu -xingquanjia fenshu -panqian_liutong panhou_liutong # 万股 -qian_zongguben hou_zongguben # 万股 -_raw -``` - -`category == 1` 时为现金分红/送转/配股,`fenhong / songzhuangu / peigu` 已归一化为每股口径。 - -### 复权公式 - -仅使用 `category == 1` 的 xdxr 记录: - -```text -factor = (pre_close - cash + rights * rights_price) / (1 + bonus + rights) -``` - -其中 `cash = fenhong`,`bonus = songzhuangu`,`rights = peigu`,`rights_price = peigujia`,`pre_close` 为事件前一日未复权收盘价。 - -- 前复权:事件日前的历史价格连续乘以各次 `factor` -- 后复权:事件日后的价格连续除以各次 `factor` - -### FundFlow(资金流向) - -``` -super_in/out large_in/out medium_in/out small_in/out -main_net_inflow total_net_inflow -``` - -### FinanceInfo(财务) - -流通股本、总股本、各省份/行业代码、资产负债表及利润表主要科目(30 个 float 字段)。 - -### CompanyInfoCategory(公司信息目录) - -``` -name filename start length -``` - -### TdxBlock(板块信息) - -``` -name category count codes -``` - -## 已知限制 - -- `get_security_list(Market.BJ, start)` 当前不能稳定获取(服务器端问题),`get_security_list_all()` 暂不纳入 BJ -- `limit_up` / `limit_down` 在 `SecurityQuote` 中默认为 `None`,涨跌停价应通过 `get_price_limits()` 计算 - -## 修复的 pytdx Bug - -| # | 位置 | 问题 | 修复 | -|---|------|------|------| -| 1 | `xdxr_info` | 循环内始终读 `body[:7]`,所有记录字段相同 | 改为从当前 `pos` 读取,pos 正确推进 | -| 2 | `security_list` | GBK 解码截断时 crash | `decode('gbk', errors='replace')` | -| 3 | `security_list` | `pre_close` 误当作整数价格 `/100` | 恢复为通达信自定义浮点解码 | -| 4 | `transaction` | 最后一个字段被 `_` 丢弃 | 保留为 `unknown_last` | -| 5 | `minute_time` | `reversed1` 字段被丢弃 | 保留为 `unknown_1` | -| 6 | `xdxr_info` | 股本字段用 `float(uint32)` 直解,差约 374 倍 | 改用 `_decode_volume`,单位万股,与 `FinanceInfo` 完全吻合 | -| 7 | `security_quotes` | 涨停/跌停价映射错误或缺失 | 停止使用不可信协议位,改由业务规则计算 | +| `get_price_limits(market, code, name, pre_close)` | 涨跌停价 | ## 架构 ``` src/easy_tdx/ -├── client.py # TdxClient / AsyncTdxClient(高层 API) +├── client.py # TdxClient / AsyncTdxClient(标准协议) +├── unified.py # UnifiedTdxClient(统一入口) +├── config.py # 服务器地址、端口、超时配置 +├── mac/ +│ ├── client.py # MacClient / AsyncMacClient(MAC 协议) +│ ├── enums.py # Period, Adjust, Category, ExMarket, SortType, ... +│ ├── models.py # MacBar, MacQuoteField, MacTick, BoardInfo, ... +│ └── commands/ # MAC 命令(build_request + parse_response,无 IO) ├── ex/ -│ ├── client.py # ExTdxClient / AsyncExTdxClient(扩展行情) -│ └── models.py # 扩展行情数据模型 -├── offline/ # 离线数据读取模块 -│ ├── paths.py # 路径检测与解析 -│ ├── daily_bar.py # 日线读取 -│ ├── min_bar.py # 分钟线读取 -│ ├── ex_daily_bar.py # 扩展市场日线 -│ ├── block.py # 板块数据读取 -│ ├── gbbq.py # 股本变迁(XOR 解密) -│ ├── history_financial.py # 历史财务数据 -│ └── finders.py # 文件路径定位 +│ ├── client.py # ExTdxClient / AsyncExTdxClient(标准协议扩展市场) +│ ├── mac_client.py # MacExClient / AsyncMacExClient(MAC 协议扩展市场) +│ └── transport/ # ExTdxConnection(端口 7727) ├── transport/ -│ ├── sync.py # TdxConnection(socket)+ ping_host / ping_all +│ ├── sync.py # TdxConnection + ping_host / ping_all │ └── async_.py # AsyncTdxConnection(asyncio) -├── commands/ # 每条命令:build_request() + parse_response(),无 IO -├── codec/ # price / volume / datetime / frame 编解码 -└── models/ # 纯 dataclass,无业务逻辑 +├── commands/ # 标准协议命令(无 IO) +├── codec/ # price / volume / datetime / frame / bitmap 编解码 +├── models/ # 纯 dataclass,无业务逻辑 +├── offline/ # 离线数据读取模块 +└── cli/ # easy-tdx CLI(click) ``` -commands 层不依赖 transport,可独立单测。transport 层负责 TCP、握手、帧解压、分发。offline 层直接读取本地二进制文件,不依赖 transport。 - -## 协议说明 - -通达信使用私有二进制 TCP 协议: - -- **帧格式**:16 字节响应头(含 zipsize / unzipsize),body 按需 zlib 解压 -- **价格编码**:变长有符号整数(类 LEB128,bit8=继续,bit7=符号,首字节低 6 位 + 后续低 7 位) -- **成交量编码**:4 字节自定义浮点(字节 3 = 指数,字节 0-2 = 精度),不可用于价格字段 -- **握手**:连接后必须顺序发送 3 条 setup 命令,响应丢弃 -- **价格存储**:整数 x 100,差分编码(相邻 tick 存 delta) +commands 层不依赖 transport,可独立单测。 ## 开发 ```bash -# 单元测试(无需网络) -python -m pytest tests/unit/ - -# 集成测试(需要网络,默认跳过) -XMTDX_LIVE=1 python -m pytest tests/integration/ - -# 类型检查 -mypy src/ - -# lint + format -ruff check src/ tests/ -ruff format --check src/ tests/ +python -m pytest tests/unit/ -v # 单元测试(无需网络) +XMTDX_LIVE=1 python -m pytest tests/integration/ -v # 集成测试 +mypy src/ # 类型检查 +ruff check src/ tests/ # lint +ruff format --check src/ tests/ # format check ``` ## 致谢 -- [pytdx](https://github.com/rainx/pytdx) — 离线数据读取模块(日线、分钟线、板块、股本变迁、历史财务的文件格式解析方法)借鉴自 pytdx 项目,感谢 rainx 及所有贡献者 -- [xmtdx](https://github.com/minionszyw/xmtdx) — 本项目的初始原型,感谢 minionszyw 的工作 -- 通达信协议分析离不开开源社区的逆向工程成果 +- [pytdx](https://github.com/rainx/pytdx) -- 离线数据读取模块借鉴自 pytdx 项目,感谢 rainx 及所有贡献者 +- [xmtdx](https://github.com/minionszyw/xmtdx) -- 本项目初始原型 +- [mootdx](https://github.com/mootdx/mootdx) -- 工程化封装参考 + +详见 [NOTICE](NOTICE) 和 [LICENSE](LICENSE)。 diff --git a/examples/01_connection/async_connect.py b/examples/01_connection/async_connect.py index 0f6a0cf..e3ac0d7 100644 --- a/examples/01_connection/async_connect.py +++ b/examples/01_connection/async_connect.py @@ -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 diff --git a/examples/01_connection/connect_best_host.py b/examples/01_connection/connect_best_host.py index c78e6d2..dabfbcb 100644 --- a/examples/01_connection/connect_best_host.py +++ b/examples/01_connection/connect_best_host.py @@ -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}") diff --git a/examples/01_connection/ping_servers.py b/examples/01_connection/ping_servers.py index 57e0d18..0063242 100644 --- a/examples/01_connection/ping_servers.py +++ b/examples/01_connection/ping_servers.py @@ -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 diff --git a/examples/02_market_info/market_stat.py b/examples/02_market_info/market_stat.py index 22f9110..39e8273 100644 --- a/examples/02_market_info/market_stat.py +++ b/examples/02_market_info/market_stat.py @@ -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 diff --git a/examples/02_market_info/security_count.py b/examples/02_market_info/security_count.py index 63446be..739775e 100644 --- a/examples/02_market_info/security_count.py +++ b/examples/02_market_info/security_count.py @@ -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 diff --git a/examples/02_market_info/security_list.py b/examples/02_market_info/security_list.py index 6660110..b4b8acc 100644 --- a/examples/02_market_info/security_list.py +++ b/examples/02_market_info/security_list.py @@ -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 diff --git a/examples/02_market_info/security_list_all.py b/examples/02_market_info/security_list_all.py index 408179a..845a0b4 100644 --- a/examples/02_market_info/security_list_all.py +++ b/examples/02_market_info/security_list_all.py @@ -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 diff --git a/examples/02_market_info/security_quotes.py b/examples/02_market_info/security_quotes.py index dda7333..6c4eb82 100644 --- a/examples/02_market_info/security_quotes.py +++ b/examples/02_market_info/security_quotes.py @@ -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 diff --git a/examples/03_kline/index_bars.py b/examples/03_kline/index_bars.py index 57284e7..1247770 100644 --- a/examples/03_kline/index_bars.py +++ b/examples/03_kline/index_bars.py @@ -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 diff --git a/examples/03_kline/security_bars.py b/examples/03_kline/security_bars.py index d52ca2b..72accbe 100644 --- a/examples/03_kline/security_bars.py +++ b/examples/03_kline/security_bars.py @@ -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 diff --git a/examples/04_minute/history_minute_data.py b/examples/04_minute/history_minute_data.py index ea83929..e89a5dc 100644 --- a/examples/04_minute/history_minute_data.py +++ b/examples/04_minute/history_minute_data.py @@ -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 diff --git a/examples/04_minute/minute_time_data.py b/examples/04_minute/minute_time_data.py index be5c2c6..31b423e 100644 --- a/examples/04_minute/minute_time_data.py +++ b/examples/04_minute/minute_time_data.py @@ -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 diff --git a/examples/05_transaction/history_transaction.py b/examples/05_transaction/history_transaction.py index 8150dbc..db92e4f 100644 --- a/examples/05_transaction/history_transaction.py +++ b/examples/05_transaction/history_transaction.py @@ -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 集合竞价 diff --git a/examples/05_transaction/transaction_data.py b/examples/05_transaction/transaction_data.py index 395f13e..e382f3c 100644 --- a/examples/05_transaction/transaction_data.py +++ b/examples/05_transaction/transaction_data.py @@ -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 集合竞价 diff --git a/examples/06_finance/company_info.py b/examples/06_finance/company_info.py index 16b3ffe..7e1fce5 100644 --- a/examples/06_finance/company_info.py +++ b/examples/06_finance/company_info.py @@ -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 diff --git a/examples/06_finance/finance_info.py b/examples/06_finance/finance_info.py index 02ee3cc..d2d8548 100644 --- a/examples/06_finance/finance_info.py +++ b/examples/06_finance/finance_info.py @@ -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 diff --git a/examples/06_finance/price_limits.py b/examples/06_finance/price_limits.py index d49370d..5145cd1 100644 --- a/examples/06_finance/price_limits.py +++ b/examples/06_finance/price_limits.py @@ -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 diff --git a/examples/06_finance/xdxr_info.py b/examples/06_finance/xdxr_info.py index cecea17..239e8a9 100644 --- a/examples/06_finance/xdxr_info.py +++ b/examples/06_finance/xdxr_info.py @@ -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 diff --git a/examples/07_block/block_info.py b/examples/07_block/block_info.py index 2f6af52..7f6ddcb 100644 --- a/examples/07_block/block_info.py +++ b/examples/07_block/block_info.py @@ -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 diff --git a/examples/08_fund_flow/fund_flow.py b/examples/08_fund_flow/fund_flow.py index 75da0dc..88a9a1c 100644 --- a/examples/08_fund_flow/fund_flow.py +++ b/examples/08_fund_flow/fund_flow.py @@ -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 diff --git a/examples/08_fund_flow/history_fund_flow.py b/examples/08_fund_flow/history_fund_flow.py index f595af9..779f883 100644 --- a/examples/08_fund_flow/history_fund_flow.py +++ b/examples/08_fund_flow/history_fund_flow.py @@ -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 diff --git a/examples/09_file_download/report_file.py b/examples/09_file_download/report_file.py index bf3e989..baddaef 100644 --- a/examples/09_file_download/report_file.py +++ b/examples/09_file_download/report_file.py @@ -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 只 diff --git a/examples/10_offline/block_data.py b/examples/10_offline/block_data.py index 7a7d8ec..2d65703 100644 --- a/examples/10_offline/block_data.py +++ b/examples/10_offline/block_data.py @@ -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... diff --git a/examples/10_offline/daily_bars.py b/examples/10_offline/daily_bars.py index b2fabea..486cf05 100644 --- a/examples/10_offline/daily_bars.py +++ b/examples/10_offline/daily_bars.py @@ -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 diff --git a/examples/10_offline/detect_home.py b/examples/10_offline/detect_home.py index 4125bd7..f52bce6 100644 --- a/examples/10_offline/detect_home.py +++ b/examples/10_offline/detect_home.py @@ -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 diff --git a/examples/10_offline/ex_daily_bars.py b/examples/10_offline/ex_daily_bars.py index 99c237f..c480df3 100644 --- a/examples/10_offline/ex_daily_bars.py +++ b/examples/10_offline/ex_daily_bars.py @@ -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 diff --git a/examples/10_offline/gbbq.py b/examples/10_offline/gbbq.py index d791701..ad5582a 100644 --- a/examples/10_offline/gbbq.py +++ b/examples/10_offline/gbbq.py @@ -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 diff --git a/examples/10_offline/history_financial.py b/examples/10_offline/history_financial.py index ddb69a4..24d3509 100644 --- a/examples/10_offline/history_financial.py +++ b/examples/10_offline/history_financial.py @@ -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 diff --git a/examples/10_offline/min_bars.py b/examples/10_offline/min_bars.py index 32cf06c..91770cf 100644 --- a/examples/10_offline/min_bars.py +++ b/examples/10_offline/min_bars.py @@ -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 diff --git a/examples/11_mac_quotes/quotes_list.py b/examples/11_mac_quotes/quotes_list.py new file mode 100644 index 0000000..7f90c20 --- /dev/null +++ b/examples/11_mac_quotes/quotes_list.py @@ -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 diff --git a/examples/11_mac_quotes/stock_quotes.py b/examples/11_mac_quotes/stock_quotes.py new file mode 100644 index 0000000..9514bb7 --- /dev/null +++ b/examples/11_mac_quotes/stock_quotes.py @@ -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 diff --git a/examples/12_mac_kline/kline_offset.py b/examples/12_mac_kline/kline_offset.py new file mode 100644 index 0000000..63c49ad --- /dev/null +++ b/examples/12_mac_kline/kline_offset.py @@ -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 diff --git a/examples/12_mac_kline/stock_kline.py b/examples/12_mac_kline/stock_kline.py new file mode 100644 index 0000000..644cd9a --- /dev/null +++ b/examples/12_mac_kline/stock_kline.py @@ -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 diff --git a/examples/13_mac_tick/chart_sampling.py b/examples/13_mac_tick/chart_sampling.py new file mode 100644 index 0000000..7685702 --- /dev/null +++ b/examples/13_mac_tick/chart_sampling.py @@ -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 diff --git a/examples/13_mac_tick/multi_day_tick.py b/examples/13_mac_tick/multi_day_tick.py new file mode 100644 index 0000000..c5dc820 --- /dev/null +++ b/examples/13_mac_tick/multi_day_tick.py @@ -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 diff --git a/examples/13_mac_tick/tick_chart.py b/examples/13_mac_tick/tick_chart.py new file mode 100644 index 0000000..e03cd2f --- /dev/null +++ b/examples/13_mac_tick/tick_chart.py @@ -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 diff --git a/examples/14_mac_transaction/transaction.py b/examples/14_mac_transaction/transaction.py new file mode 100644 index 0000000..9902406 --- /dev/null +++ b/examples/14_mac_transaction/transaction.py @@ -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 diff --git a/examples/15_mac_board/belong_board.py b/examples/15_mac_board/belong_board.py new file mode 100644 index 0000000..00f4a41 --- /dev/null +++ b/examples/15_mac_board/belong_board.py @@ -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 diff --git a/examples/15_mac_board/board_list.py b/examples/15_mac_board/board_list.py new file mode 100644 index 0000000..8314a8b --- /dev/null +++ b/examples/15_mac_board/board_list.py @@ -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 diff --git a/examples/15_mac_board/board_members.py b/examples/15_mac_board/board_members.py new file mode 100644 index 0000000..f5eef9b --- /dev/null +++ b/examples/15_mac_board/board_members.py @@ -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 diff --git a/examples/16_mac_capital/capital_flow.py b/examples/16_mac_capital/capital_flow.py new file mode 100644 index 0000000..d017a58 --- /dev/null +++ b/examples/16_mac_capital/capital_flow.py @@ -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 diff --git a/examples/17_mac_monitor/auction.py b/examples/17_mac_monitor/auction.py new file mode 100644 index 0000000..e750edf --- /dev/null +++ b/examples/17_mac_monitor/auction.py @@ -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 diff --git a/examples/17_mac_monitor/server_info.py b/examples/17_mac_monitor/server_info.py new file mode 100644 index 0000000..e8144f0 --- /dev/null +++ b/examples/17_mac_monitor/server_info.py @@ -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 diff --git a/examples/17_mac_monitor/symbol_info.py b/examples/17_mac_monitor/symbol_info.py new file mode 100644 index 0000000..d0e6383 --- /dev/null +++ b/examples/17_mac_monitor/symbol_info.py @@ -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 diff --git a/examples/17_mac_monitor/unusual.py b/examples/17_mac_monitor/unusual.py new file mode 100644 index 0000000..d28aba2 --- /dev/null +++ b/examples/17_mac_monitor/unusual.py @@ -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 diff --git a/examples/18_mac_ex/ex_goods_list.py b/examples/18_mac_ex/ex_goods_list.py new file mode 100644 index 0000000..11ae072 --- /dev/null +++ b/examples/18_mac_ex/ex_goods_list.py @@ -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 diff --git a/examples/18_mac_ex/ex_kline.py b/examples/18_mac_ex/ex_kline.py new file mode 100644 index 0000000..ffd49fb --- /dev/null +++ b/examples/18_mac_ex/ex_kline.py @@ -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 diff --git a/examples/18_mac_ex/ex_quotes.py b/examples/18_mac_ex/ex_quotes.py new file mode 100644 index 0000000..55c499a --- /dev/null +++ b/examples/18_mac_ex/ex_quotes.py @@ -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 diff --git a/examples/18_mac_ex/ex_tick_chart.py b/examples/18_mac_ex/ex_tick_chart.py new file mode 100644 index 0000000..7f495d9 --- /dev/null +++ b/examples/18_mac_ex/ex_tick_chart.py @@ -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 diff --git a/examples/19_unified/unified_client.py b/examples/19_unified/unified_client.py new file mode 100644 index 0000000..8e00c58 --- /dev/null +++ b/examples/19_unified/unified_client.py @@ -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 diff --git a/examples/20_cli/cli_examples.sh b/examples/20_cli/cli_examples.sh new file mode 100644 index 0000000..f5baacf --- /dev/null +++ b/examples/20_cli/cli_examples.sh @@ -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 线。参数: <代码> --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. 获取扩展市场报价(美股苹果)===" +# 获取单只扩展市场股票报价。参数: <代码> +# 返回列: 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. 获取扩展市场商品列表(港股主板)===" +# 获取扩展市场商品列表。参数: --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. 获取扩展市场分时图(港股腾讯)===" +# 获取扩展市场当日分时走势。参数: <代码> +# 返回列: 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条) diff --git a/pyproject.toml b/pyproject.toml index 9650f07..29a8b21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/src/easy_tdx/__init__.py b/src/easy_tdx/__init__.py index 1d5c198..b18db99 100644 --- a/src/easy_tdx/__init__.py +++ b/src/easy_tdx/__init__.py @@ -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" diff --git a/src/easy_tdx/cli/__init__.py b/src/easy_tdx/cli/__init__.py new file mode 100644 index 0000000..263fce4 --- /dev/null +++ b/src/easy_tdx/cli/__init__.py @@ -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) diff --git a/src/easy_tdx/cli/cmd_admin.py b/src/easy_tdx/cli/cmd_admin.py new file mode 100644 index 0000000..4792585 --- /dev/null +++ b/src/easy_tdx/cli/cmd_admin.py @@ -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") diff --git a/src/easy_tdx/cli/cmd_auction.py b/src/easy_tdx/cli/cmd_auction.py new file mode 100644 index 0000000..df318d2 --- /dev/null +++ b/src/easy_tdx/cli/cmd_auction.py @@ -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) diff --git a/src/easy_tdx/cli/cmd_board.py b/src/easy_tdx/cli/cmd_board.py new file mode 100644 index 0000000..2101708 --- /dev/null +++ b/src/easy_tdx/cli/cmd_board.py @@ -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) diff --git a/src/easy_tdx/cli/cmd_capital.py b/src/easy_tdx/cli/cmd_capital.py new file mode 100644 index 0000000..bafad60 --- /dev/null +++ b/src/easy_tdx/cli/cmd_capital.py @@ -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) diff --git a/src/easy_tdx/cli/cmd_ex.py b/src/easy_tdx/cli/cmd_ex.py new file mode 100644 index 0000000..067c932 --- /dev/null +++ b/src/easy_tdx/cli/cmd_ex.py @@ -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") diff --git a/src/easy_tdx/cli/cmd_finance.py b/src/easy_tdx/cli/cmd_finance.py new file mode 100644 index 0000000..c12c00c --- /dev/null +++ b/src/easy_tdx/cli/cmd_finance.py @@ -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") diff --git a/src/easy_tdx/cli/cmd_info.py b/src/easy_tdx/cli/cmd_info.py new file mode 100644 index 0000000..3be9637 --- /dev/null +++ b/src/easy_tdx/cli/cmd_info.py @@ -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) diff --git a/src/easy_tdx/cli/cmd_kline.py b/src/easy_tdx/cli/cmd_kline.py new file mode 100644 index 0000000..f43ced6 --- /dev/null +++ b/src/easy_tdx/cli/cmd_kline.py @@ -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) diff --git a/src/easy_tdx/cli/cmd_monitor.py b/src/easy_tdx/cli/cmd_monitor.py new file mode 100644 index 0000000..bcd863d --- /dev/null +++ b/src/easy_tdx/cli/cmd_monitor.py @@ -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) diff --git a/src/easy_tdx/cli/cmd_quote.py b/src/easy_tdx/cli/cmd_quote.py new file mode 100644 index 0000000..0507d4a --- /dev/null +++ b/src/easy_tdx/cli/cmd_quote.py @@ -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) diff --git a/src/easy_tdx/cli/cmd_tick.py b/src/easy_tdx/cli/cmd_tick.py new file mode 100644 index 0000000..ecc2207 --- /dev/null +++ b/src/easy_tdx/cli/cmd_tick.py @@ -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) diff --git a/src/easy_tdx/cli/cmd_transaction.py b/src/easy_tdx/cli/cmd_transaction.py new file mode 100644 index 0000000..93fcef3 --- /dev/null +++ b/src/easy_tdx/cli/cmd_transaction.py @@ -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) diff --git a/src/easy_tdx/cli/conn.py b/src/easy_tdx/cli/conn.py new file mode 100644 index 0000000..4259da6 --- /dev/null +++ b/src/easy_tdx/cli/conn.py @@ -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() diff --git a/src/easy_tdx/cli/output.py b/src/easy_tdx/cli/output.py new file mode 100644 index 0000000..36b897d --- /dev/null +++ b/src/easy_tdx/cli/output.py @@ -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) diff --git a/src/easy_tdx/cli/parsers.py b/src/easy_tdx/cli/parsers.py new file mode 100644 index 0000000..a2da55b --- /dev/null +++ b/src/easy_tdx/cli/parsers.py @@ -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 diff --git a/src/easy_tdx/client.py b/src/easy_tdx/client.py index 737f9fd..8cca5dc 100644 --- a/src/easy_tdx/client.py +++ b/src/easy_tdx/client.py @@ -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) ) diff --git a/src/easy_tdx/codec/bitmap.py b/src/easy_tdx/codec/bitmap.py new file mode 100644 index 0000000..f73b859 --- /dev/null +++ b/src/easy_tdx/codec/bitmap.py @@ -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 = " "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, " 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 diff --git a/src/easy_tdx/codec/mac_frame.py b/src/easy_tdx/codec/mac_frame.py new file mode 100644 index 0000000..fac43ed --- /dev/null +++ b/src/easy_tdx/codec/mac_frame.py @@ -0,0 +1,50 @@ +"""MAC 协议请求帧构建。 + +MAC 协议请求帧格式(10 字节头 + body): + struct " 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(" ~/.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) diff --git a/src/easy_tdx/ex/__init__.py b/src/easy_tdx/ex/__init__.py index aa1440d..9733763 100644 --- a/src/easy_tdx/ex/__init__.py +++ b/src/easy_tdx/ex/__init__.py @@ -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", ] diff --git a/src/easy_tdx/ex/client.py b/src/easy_tdx/ex/client.py index 05b02e8..1802408 100644 --- a/src/easy_tdx/ex/client.py +++ b/src/easy_tdx/ex/client.py @@ -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 diff --git a/src/easy_tdx/ex/commands/get_instrument_count.py b/src/easy_tdx/ex/commands/get_instrument_count.py index d744487..0da7a23 100644 --- a/src/easy_tdx/ex/commands/get_instrument_count.py +++ b/src/easy_tdx/ex/commands/get_instrument_count.py @@ -14,4 +14,4 @@ class GetExInstrumentCountCmd(BaseCommand[int]): if len(body) < 23: return 0 (count,) = unpack_from(" bytes: + inner = struct.pack(" bool: + # Login 响应 body 非空即视为成功 + return len(body) >= 2 diff --git a/src/easy_tdx/ex/mac_client.py b/src/easy_tdx/ex/mac_client.py new file mode 100644 index 0000000..d41e66f --- /dev/null +++ b/src/easy_tdx/ex/mac_client.py @@ -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) diff --git a/src/easy_tdx/ex/models.py b/src/easy_tdx/ex/models.py index f50021e..adf734a 100644 --- a/src/easy_tdx/ex/models.py +++ b/src/easy_tdx/ex/models.py @@ -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 diff --git a/src/easy_tdx/ex/transport/async_.py b/src/easy_tdx/ex/transport/async_.py index 56142ca..263731c 100644 --- a/src/easy_tdx/ex/transport/async_.py +++ b/src/easy_tdx/ex/transport/async_.py @@ -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( diff --git a/src/easy_tdx/ex/transport/sync.py b/src/easy_tdx/ex/transport/sync.py index 0bada44..ae128f8 100644 --- a/src/easy_tdx/ex/transport/sync.py +++ b/src/easy_tdx/ex/transport/sync.py @@ -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) diff --git a/src/easy_tdx/mac/__init__.py b/src/easy_tdx/mac/__init__.py new file mode 100644 index 0000000..8b8a68c --- /dev/null +++ b/src/easy_tdx/mac/__init__.py @@ -0,0 +1 @@ +"""MAC 协议客户端(板块、竞价、复权K线等高级接口)。""" diff --git a/src/easy_tdx/mac/client.py b/src/easy_tdx/mac/client.py new file mode 100644 index 0000000..1097cfa --- /dev/null +++ b/src/easy_tdx/mac/client.py @@ -0,0 +1,1257 @@ +"""MAC 协议高层 API:MacClient(同步)和 AsyncMacClient(asyncio)。""" + +from __future__ import annotations + +import asyncio +import time +from dataclasses import asdict +from types import TracebackType +from typing import Any, TypeVar + +import pandas as pd + +from .._df import _to_df +from ..commands.base import BaseCommand +from ..config import get_best_host, get_mac_hosts, get_port, get_timeout, save_best_host +from ..exceptions import TdxConnectionError +from ..transport.async_ import AsyncTdxConnection +from ..transport.sync import TdxConnection, ping_mac_all +from .commands import ( + BoardListCmd, + BoardMembersQuotesCmd, + KlineOffsetCmd, + ServerInfoCmd, + SymbolAuctionCmd, + SymbolBarCmd, + SymbolBelongBoardCmd, + SymbolCapitalFlowCmd, + SymbolInfoCmd, + SymbolQuotesCmd, + SymbolTickChartCmd, + SymbolTransactionCmd, + TickChartsCmd, + UnusualCmd, +) +from .commands.chart_sampling import ChartSamplingCmd +from .commands.file_query import FileDownloadCmd, FileListCmd +from .commands.goods_list import GoodsListCmd +from ..codec.bitmap import Fields, PresetField +from .enums import Adjust, BoardType, Category, FilterType, Period, SortOrder, SortType +from .models import ( + MacBar, + MacMultiTickChart, + MacQuoteField, + MacTickChart, +) + +_RETRY_DELAYS = (0.1, 0.5, 1.0, 2.0) +_KLINE_PAGE_SIZE = 700 +_BOARD_MEMBERS_PAGE_SIZE = 80 + + +def _convert_board_code(board_symbol: str) -> int: + """将用户可见的板块代码转换为服务器协议代码。 + + 转换规则(来自 opentdx exchange_board_code): + US0401 → 30401 (30000 + N) + HK0283 → 20283 (20000 + N) + 000686 → 31686 (31000 + N) + 399372 → 30372 (N - 399000 + 30000) + 899050 → 32050 (N - 899000 + 32000) + 880686 → 20686 (N - 880000 + 20000) + 其他 → int(N) + """ + s = board_symbol.strip() + if s.startswith("US"): + return 30000 + int(s[2:]) + if s.startswith("HK"): + return 20000 + int(s[2:]) + if len(s) == 6: + if s.startswith("88"): + return int(s) - 880000 + 20000 + if s.startswith("399"): + return int(s) - 399000 + 30000 + if s.startswith("899"): + return int(s) - 899000 + 32000 + if s.startswith("000"): + return 31000 + int(s) + return int(s) +_TRANSACTION_PAGE_SIZE = 1000 + +_T = TypeVar("_T") + + +def _flatten_quote_fields(quotes: list[MacQuoteField]) -> list[dict[str, Any]]: + """将 MacQuoteField 展平为 DataFrame 友好的 dict 列表。""" + rows: list[dict[str, Any]] = [] + for q in quotes: + d: dict[str, Any] = {"market": q.market, "code": q.code, "name": q.name} + d.update(q.fields) + rows.append(d) + return rows + + +def _quotes_to_df(quotes: list[MacQuoteField]) -> pd.DataFrame: + return pd.DataFrame(_flatten_quote_fields(quotes)) + + +def _flatten_tick_chart(chart: MacTickChart) -> list[dict[str, Any]]: + """将 MacTickChart 的 ticks 展平为 DataFrame 行。""" + rows: list[dict[str, Any]] = [] + for tick in chart.charts: + rows.append(asdict(tick)) + return rows + + +def _flatten_multi_tick_chart(chart: MacMultiTickChart) -> list[dict[str, Any]]: + """将 MacMultiTickChart 的所有天的 ticks 展平为 DataFrame 行。""" + rows: list[dict[str, Any]] = [] + for day in chart.charts: + for tick in day.ticks: + d = asdict(tick) + d["date"] = day.date + d["pre_close"] = day.pre_close + rows.append(d) + return rows + + +# ============================================================ +# 同步客户端 +# ============================================================ + + +class MacClient: + """同步 MAC 协议客户端,支持 IP 优选与断线自动重连。 + + 使用示例:: + + with MacClient("121.36.248.138") as c: + df = c.get_stock_kline(0, "600000", Period.DAILY, count=100) + + # 自动选延迟最低的 MAC 服务器 + with MacClient.from_best_host() as c: + df = c.get_board_list() + """ + + def __init__( + self, + 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 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(self._host, self._port, self._timeout) + + # ------------------------------------------------------------------ # + # 工厂方法 + # ------------------------------------------------------------------ # + + @classmethod + def from_best_host( + cls, + 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, + ) -> MacClient: + """测量所有 MAC 服务器延迟,选最低延迟的建立客户端。自动保存最佳主机。""" + if hosts is None: + hosts = get_mac_hosts() + if port is None: + port = get_port() + if timeout is None: + timeout = get_timeout() + ranked = ping_mac_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] | None = None, + port: int | None = None, + timeout: float = 5.0, + ) -> list[tuple[str, float]]: + """测量多台 MAC 服务器延迟,返回按延迟排序的 (host, seconds) 列表。""" + if hosts is None: + hosts = get_mac_hosts() + if port is None: + port = get_port() + return ping_mac_all(hosts, port, timeout) + + # ------------------------------------------------------------------ # + # 连接管理 + # ------------------------------------------------------------------ # + + 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(KlineOffsetCmd(0, 1)) + 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) -> MacClient: + self.connect() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + # ------------------------------------------------------------------ # + # 内部执行:含自动重连 + # ------------------------------------------------------------------ # + + def _execute(self, cmd: BaseCommand[_T]) -> _T: + """执行命令;断线时指数退避重试。""" + try: + return self._conn.execute(cmd) + except TdxConnectionError: + if not self._auto_reconnect: + raise + 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] + + # ------------------------------------------------------------------ # + # 报价 + # ------------------------------------------------------------------ # + + def get_stock_quotes( + self, + stocks: list[tuple[int, str]], + fields: object = None, + ) -> pd.DataFrame: + """批量获取自定义字段报价(最多80只/次)。 + + Args: + stocks: [(market, code), ...] 列表。 + fields: 字段选择,默认 PresetField.COMMON。 + """ + quotes = self._execute(SymbolQuotesCmd(stocks, fields)) # type: ignore[arg-type] + return _quotes_to_df(quotes) + + def get_stock_quotes_list( + self, + category: Category, + start: int = 0, + count: int = 80, + sort_type: SortType = SortType.CHANGE_PCT, + sort_order: SortOrder = SortOrder.DESC, + exclude_flags: list[FilterType] | None = None, + fields: Fields | None = None, + ) -> pd.DataFrame: + """获取市场分类报价列表(自动分页)。 + + Args: + category: 市场分类(如 Category.A, Category.SH, Category.KCB 等)。 + start: 起始偏移。 + count: 请求总数。 + sort_type: 排序字段。 + sort_order: 排序方向。 + exclude_flags: 过滤标志列表。 + fields: 请求字段集合,默认 PresetField.BASIC + PresetField.VOLUME。 + """ + if fields is None: + fields = PresetField.BASIC + PresetField.VOLUME + all_quotes: list[MacQuoteField] = [] + fetched = 0 + page_size = min(count, _BOARD_MEMBERS_PAGE_SIZE) + offset = start + + while fetched < count: + batch = self._execute( + BoardMembersQuotesCmd( + board_code=int(category), + sort_type=sort_type, + start=offset, + page_size=page_size, + sort_order=sort_order, + fields=fields, + exclude_flags=exclude_flags, + ) + ) + if not batch: + break + all_quotes = batch + all_quotes + fetched += len(batch) + offset += len(batch) + if len(batch) < page_size: + break + + return _quotes_to_df(all_quotes) + + # ------------------------------------------------------------------ # + # K 线(支持复权) + # ------------------------------------------------------------------ # + + def get_stock_kline( + self, + market: int, + code: str, + period: Period = Period.DAILY, + start: int = 0, + count: int = 800, + times: int = 1, + adjust: Adjust = Adjust.NONE, + ) -> pd.DataFrame: + """获取 K 线数据(自动分页,每页最多 700 条)。 + + Args: + market: 市场代码。 + code: 股票代码。 + period: K 线周期。 + start: 起始偏移(0 = 最新)。 + count: 总请求条数。 + times: 周期倍数(Period.MINS/DAYS 时有效)。 + adjust: 复权方式。 + """ + all_bars: list[MacBar] = [] + fetched = 0 + offset = start + + while fetched < count: + page_size = min(count - fetched, _KLINE_PAGE_SIZE) + bars = self._execute( + SymbolBarCmd( + market=market, + code=code, + period=period, + times=times, + start=offset, + count=page_size, + fq=adjust, + ) + ) + if not bars: + break + all_bars = bars + all_bars + fetched += len(bars) + offset += len(bars) + if len(bars) < page_size: + break + + return _to_df(all_bars) + + # ------------------------------------------------------------------ # + # 分时 + # ------------------------------------------------------------------ # + + def get_tick_chart( + self, + market: int, + code: str, + date: int | None = None, + ) -> pd.DataFrame: + """获取单日分时图。 + + Args: + market: 市场代码。 + code: 股票代码。 + date: 查询日期(YYYYMMDD),None 表示今天。 + """ + from datetime import date as date_cls + + query_date = ( + date_cls(date // 10000, (date % 10000) // 100, date % 100) + if date is not None else None + ) + chart = self._execute(SymbolTickChartCmd(market, code, query_date)) + return pd.DataFrame(_flatten_tick_chart(chart)) + + def get_tick_charts( + self, + market: int, + code: str, + date: int | None = None, + days: int = 5, + ) -> pd.DataFrame: + """获取多日分时图(最多 5 天)。 + + Args: + market: 市场代码。 + code: 股票代码。 + date: 起始日期(YYYYMMDD),None 表示从最新交易日开始。 + days: 天数。 + """ + from datetime import date as date_cls + + start_date = ( + date_cls(date // 10000, (date % 10000) // 100, date % 100) + if date is not None else None + ) + chart = self._execute(TickChartsCmd(market, code, start_date, days)) + return pd.DataFrame(_flatten_multi_tick_chart(chart)) + + def get_chart_sampling(self, market: int, code: str) -> pd.DataFrame: + """获取分时缩略采样价格点(240 个点)。 + + Args: + market: 市场代码。 + code: 股票代码。 + """ + prices = self._execute(ChartSamplingCmd(market, code)) + return pd.DataFrame({"price": prices}) + + # ------------------------------------------------------------------ # + # 逐笔成交 + # ------------------------------------------------------------------ # + + def get_transactions( + self, + market: int, + code: str, + count: int = 2000, + start: int = 0, + date: int | None = None, + ) -> pd.DataFrame: + """获取逐笔成交数据(自动分页)。 + + Args: + market: 市场代码。 + code: 股票代码。 + count: 请求总数。 + start: 起始偏移。 + date: 查询日期(YYYYMMDD),None 表示今天。 + """ + from datetime import date as date_cls + + query_date = ( + date_cls(date // 10000, (date % 10000) // 100, date % 100) + if date is not None else None + ) + all_items = self._execute( + SymbolTransactionCmd( + market, code, query_date, start, min(count, _TRANSACTION_PAGE_SIZE) + ) + ) + fetched = len(all_items) + offset = start + fetched + + while fetched < count: + page_size = min(count - fetched, _TRANSACTION_PAGE_SIZE) + batch = self._execute(SymbolTransactionCmd(market, code, query_date, offset, page_size)) + if not batch: + break + all_items.extend(batch) + fetched += len(batch) + offset += len(batch) + if len(batch) < page_size: + break + + return _to_df(all_items) + + # ------------------------------------------------------------------ # + # 个股信息 + # ------------------------------------------------------------------ # + + def get_symbol_info(self, market: int, code: str) -> pd.DataFrame: + """获取个股简要特征快照。 + + Args: + market: 市场代码。 + code: 股票代码。 + """ + info = self._execute(SymbolInfoCmd(market, code)) + return _to_df(info) + + # ------------------------------------------------------------------ # + # 板块 + # ------------------------------------------------------------------ # + + def get_board_list( + self, + board_type: BoardType = BoardType.ALL, + count: int = 10000, + ) -> pd.DataFrame: + """获取板块列表(自动分页)。 + + Args: + board_type: 板块类型。 + count: 请求总数。 + """ + all_items = self._execute(BoardListCmd(board_type, 0, min(count, 150))) + fetched = len(all_items) + offset = fetched + + while fetched < count: + page_size = min(count - fetched, 150) + batch = self._execute(BoardListCmd(board_type, offset, page_size)) + if not batch: + break + all_items.extend(batch) + fetched += len(batch) + offset += len(batch) + if len(batch) < page_size: + break + + return _to_df(all_items) + + def get_board_members( + self, + board_symbol: str, + count: int = 100000, + sort_type: SortType = SortType.CHANGE_PCT, + sort_order: SortOrder = SortOrder.DESC, + fields: object = PresetField.COMMON, + exclude_flags: list[FilterType] | None = None, + ) -> pd.DataFrame: + """获取板块成分股报价(自动分页)。 + + Args: + board_symbol: 板块代码(如 "881001")。 + count: 请求总数。 + sort_type: 排序字段。 + sort_order: 排序方向。 + fields: 字段选择。 + exclude_flags: 过滤标志列表。 + """ + board_code = _convert_board_code(board_symbol) + all_quotes: list[MacQuoteField] = [] + fetched = 0 + offset = 0 + + while fetched < count: + page_size = min(count - fetched, _BOARD_MEMBERS_PAGE_SIZE) + batch = self._execute( + BoardMembersQuotesCmd( + board_code=board_code, + sort_type=sort_type, + start=offset, + page_size=page_size, + sort_order=sort_order, + fields=fields, # type: ignore[arg-type] + exclude_flags=exclude_flags, + ) + ) + if not batch: + break + all_quotes = batch + all_quotes + fetched += len(batch) + offset += len(batch) + if len(batch) < page_size: + break + + return _quotes_to_df(all_quotes) + + def get_belong_board(self, market: int, code: str) -> pd.DataFrame: + """获取个股所属板块列表。 + + Args: + market: 市场代码。 + code: 股票代码。 + """ + items = self._execute(SymbolBelongBoardCmd(market, code)) + return _to_df(items) + + # ------------------------------------------------------------------ # + # 资金流向 + # ------------------------------------------------------------------ # + + def get_capital_flow(self, market: int, code: str) -> pd.DataFrame: + """获取个股资金流向。 + + Args: + market: 市场代码。 + code: 股票代码。 + """ + data = self._execute(SymbolCapitalFlowCmd(market, code)) + if data is None: + return pd.DataFrame() + return _to_df(data) + + # ------------------------------------------------------------------ # + # 集合竞价 + # ------------------------------------------------------------------ # + + def get_auction(self, market: int, code: str) -> pd.DataFrame: + """获取集合竞价数据。 + + Args: + market: 市场代码。 + code: 股票代码。 + """ + items = self._execute(SymbolAuctionCmd(market, code)) + return _to_df(items) + + # ------------------------------------------------------------------ # + # 异动 + # ------------------------------------------------------------------ # + + def get_unusual( + self, + market: int, + start: int = 0, + count: int = 0, + ) -> pd.DataFrame: + """获取市场异动数据。 + + Args: + market: 市场代码。 + start: 起始偏移。 + count: 请求数量(0 表示使用默认值 600)。 + """ + items = self._execute(UnusualCmd(market, start, count or 600)) + return _to_df(items) + + # ------------------------------------------------------------------ # + # 服务器信息 + # ------------------------------------------------------------------ # + + def get_server_info(self) -> pd.DataFrame: + """获取服务器交易时段信息。""" + info = self._execute(ServerInfoCmd()) + return _to_df(info) + + def get_kline_offset( + self, + offset: int = 0, + count: int = 128000, + ) -> pd.DataFrame: + """获取 K 线数据偏移信息。 + + Args: + offset: 偏移量。 + count: 请求数量。 + """ + info = self._execute(KlineOffsetCmd(offset, count)) + return _to_df(info) + + # ------------------------------------------------------------------ # + # 文件操作 + # ------------------------------------------------------------------ # + + def get_file_meta(self, filename: str) -> pd.DataFrame: + """查询远程文件元信息。 + + Args: + filename: 远程文件名。 + """ + meta = self._execute(FileListCmd(filename)) + return _to_df(meta) + + def download_file_chunk( + self, + filename: str, + index: int, + offset: int, + size: int, + ) -> bytes: + """下载远程文件的一个分片。 + + Args: + filename: 远程文件名。 + index: 分段序号(1-based)。 + offset: 字节偏移。 + size: 请求块大小。 + """ + return self._execute(FileDownloadCmd(filename, index, offset, size)) + + def download_file( + self, + filename: str, + filesize: int = 0, + ) -> bytearray: + """下载完整远程文件。 + + Args: + filename: 远程文件名。 + filesize: 预期文件大小(0 表示自动检测)。 + """ + if filesize <= 0: + meta = self._execute(FileListCmd(filename)) + filesize = meta.size + + full_data = bytearray() + chunk_size = 30000 + pos = 0 + idx = 1 + + while pos < filesize: + chunk = self._execute(FileDownloadCmd(filename, idx, pos, chunk_size)) + if not chunk: + break + full_data.extend(chunk) + pos += len(chunk) + idx += 1 + if len(chunk) < chunk_size: + break + + return full_data + + # ------------------------------------------------------------------ # + # 扩展市场 + # ------------------------------------------------------------------ # + + def get_goods_list( + self, + market: int, + start: int = 0, + count: int = 600, + ) -> pd.DataFrame: + """获取扩展市场(期货/期权等)商品列表。 + + Args: + market: 扩展市场代码(ExMarket 枚举值)。 + start: 起始偏移。 + count: 请求数量(最大 1000)。 + """ + items = self._execute(GoodsListCmd(market, start, count)) + return _to_df(items) + + +# ============================================================ +# 异步客户端 +# ============================================================ + + +class AsyncMacClient: + """异步 MAC 协议客户端(asyncio)。 + + 使用示例:: + + async with AsyncMacClient("121.36.248.138") as c: + df = await c.get_stock_kline(0, "600000", Period.DAILY, count=100) + + 注意: + 单个 AsyncMacClient 仅维护一条 TCP 连接;并发调用会在连接内串行执行。 + """ + + def __init__( + self, + 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 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(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] | None = None, + port: int | None = None, + timeout: float | None = None, + ping_timeout: float = 5.0, + auto_reconnect: bool = True, + heartbeat_interval: float = 15.0, + ) -> AsyncMacClient: + """测量所有 MAC 服务器延迟,选最低延迟的建立客户端。自动保存最佳主机。""" + if hosts is None: + hosts = get_mac_hosts() + if port is None: + port = get_port() + if timeout is None: + timeout = get_timeout() + ranked = ping_mac_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] | None = None, + port: int | None = None, + timeout: float = 5.0, + ) -> list[tuple[str, float]]: + """测量多台 MAC 服务器延迟。""" + if hosts is None: + hosts = get_mac_hosts() + if port is None: + port = get_port() + return ping_mac_all(hosts, port, timeout) + + # ------------------------------------------------------------------ # + # 连接管理 + # ------------------------------------------------------------------ # + + async def connect(self) -> None: + await self._conn.connect() + self._start_heartbeat() + + async def close(self) -> None: + await self._stop_heartbeat() + await self._conn.close() + + async def disconnect(self) -> None: + """Alias for close().""" + await self.close() + + async def ensure_connected(self) -> None: + """验证连接存活,断线则自动重建。""" + try: + await self._execute(KlineOffsetCmd(0, 1)) + except TdxConnectionError: + await self._stop_heartbeat() + await self._conn.close() + self._conn = AsyncTdxConnection(self._host, self._port, self._timeout) + await self._conn.connect() + self._start_heartbeat() + + async def __aenter__(self) -> AsyncMacClient: + 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(KlineOffsetCmd(0, 1)) + except asyncio.CancelledError: + break + except Exception: + pass + + # ------------------------------------------------------------------ # + # 内部执行 + # ------------------------------------------------------------------ # + + 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 + 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_stock_quotes( + self, + stocks: list[tuple[int, str]], + fields: object = None, + ) -> pd.DataFrame: + quotes = await self._execute(SymbolQuotesCmd(stocks, fields)) # type: ignore[arg-type] + return _quotes_to_df(quotes) + + async def get_stock_quotes_list( + self, + category: Category, + start: int = 0, + count: int = 80, + sort_type: SortType = SortType.CHANGE_PCT, + sort_order: SortOrder = SortOrder.DESC, + exclude_flags: list[FilterType] | None = None, + fields: Fields | None = None, + ) -> pd.DataFrame: + if fields is None: + fields = PresetField.BASIC + PresetField.VOLUME + all_quotes: list[MacQuoteField] = [] + fetched = 0 + page_size = min(count, _BOARD_MEMBERS_PAGE_SIZE) + offset = start + + while fetched < count: + batch = await self._execute( + BoardMembersQuotesCmd( + board_code=int(category), + sort_type=sort_type, + start=offset, + page_size=page_size, + sort_order=sort_order, + fields=fields, + exclude_flags=exclude_flags, + ) + ) + if not batch: + break + all_quotes = batch + all_quotes + fetched += len(batch) + offset += len(batch) + if len(batch) < page_size: + break + + return _quotes_to_df(all_quotes) + + # ------------------------------------------------------------------ # + # K 线 + # ------------------------------------------------------------------ # + + async def get_stock_kline( + self, + market: int, + code: str, + period: Period = Period.DAILY, + start: int = 0, + count: int = 800, + times: int = 1, + adjust: Adjust = Adjust.NONE, + ) -> pd.DataFrame: + all_bars: list[MacBar] = [] + fetched = 0 + offset = start + + while fetched < count: + page_size = min(count - fetched, _KLINE_PAGE_SIZE) + bars = await self._execute( + SymbolBarCmd( + market=market, + code=code, + period=period, + times=times, + start=offset, + count=page_size, + fq=adjust, + ) + ) + if not bars: + break + all_bars = bars + all_bars + fetched += len(bars) + offset += len(bars) + if len(bars) < page_size: + break + + return _to_df(all_bars) + + # ------------------------------------------------------------------ # + # 分时 + # ------------------------------------------------------------------ # + + async def get_tick_chart( + self, + market: int, + code: str, + date: int | None = None, + ) -> pd.DataFrame: + from datetime import date as date_cls + + query_date = ( + date_cls(date // 10000, (date % 10000) // 100, date % 100) + if date is not None else None + ) + chart = await self._execute(SymbolTickChartCmd(market, code, query_date)) + return pd.DataFrame(_flatten_tick_chart(chart)) + + async def get_tick_charts( + self, + market: int, + code: str, + date: int | None = None, + days: int = 5, + ) -> pd.DataFrame: + from datetime import date as date_cls + + start_date = ( + date_cls(date // 10000, (date % 10000) // 100, date % 100) + if date is not None else None + ) + chart = await self._execute(TickChartsCmd(market, code, start_date, days)) + return pd.DataFrame(_flatten_multi_tick_chart(chart)) + + async def get_chart_sampling(self, market: int, code: str) -> pd.DataFrame: + prices = await self._execute(ChartSamplingCmd(market, code)) + return pd.DataFrame({"price": prices}) + + # ------------------------------------------------------------------ # + # 逐笔成交 + # ------------------------------------------------------------------ # + + async def get_transactions( + self, + market: int, + code: str, + count: int = 2000, + start: int = 0, + date: int | None = None, + ) -> pd.DataFrame: + from datetime import date as date_cls + + query_date = ( + date_cls(date // 10000, (date % 10000) // 100, date % 100) + if date is not None else None + ) + all_items = await self._execute( + SymbolTransactionCmd( + market, code, query_date, start, min(count, _TRANSACTION_PAGE_SIZE) + ) + ) + fetched = len(all_items) + offset = start + fetched + + while fetched < count: + page_size = min(count - fetched, _TRANSACTION_PAGE_SIZE) + batch = await self._execute( + SymbolTransactionCmd(market, code, query_date, offset, page_size) + ) + if not batch: + break + all_items.extend(batch) + fetched += len(batch) + offset += len(batch) + if len(batch) < page_size: + break + + return _to_df(all_items) + + # ------------------------------------------------------------------ # + # 个股信息 + # ------------------------------------------------------------------ # + + async def get_symbol_info(self, market: int, code: str) -> pd.DataFrame: + info = await self._execute(SymbolInfoCmd(market, code)) + return _to_df(info) + + # ------------------------------------------------------------------ # + # 板块 + # ------------------------------------------------------------------ # + + async def get_board_list( + self, + board_type: BoardType = BoardType.ALL, + count: int = 10000, + ) -> pd.DataFrame: + all_items = await self._execute(BoardListCmd(board_type, 0, min(count, 150))) + fetched = len(all_items) + offset = fetched + + while fetched < count: + page_size = min(count - fetched, 150) + batch = await self._execute(BoardListCmd(board_type, offset, page_size)) + if not batch: + break + all_items.extend(batch) + fetched += len(batch) + offset += len(batch) + if len(batch) < page_size: + break + + return _to_df(all_items) + + async def get_board_members( + self, + board_symbol: str, + count: int = 100000, + sort_type: SortType = SortType.CHANGE_PCT, + sort_order: SortOrder = SortOrder.DESC, + fields: object = PresetField.COMMON, + exclude_flags: list[FilterType] | None = None, + ) -> pd.DataFrame: + board_code = _convert_board_code(board_symbol) + all_quotes: list[MacQuoteField] = [] + fetched = 0 + offset = 0 + + while fetched < count: + page_size = min(count - fetched, _BOARD_MEMBERS_PAGE_SIZE) + batch = await self._execute( + BoardMembersQuotesCmd( + board_code=board_code, + sort_type=sort_type, + start=offset, + page_size=page_size, + sort_order=sort_order, + fields=fields, # type: ignore[arg-type] + exclude_flags=exclude_flags, + ) + ) + if not batch: + break + all_quotes = batch + all_quotes + fetched += len(batch) + offset += len(batch) + if len(batch) < page_size: + break + + return _quotes_to_df(all_quotes) + + async def get_belong_board(self, market: int, code: str) -> pd.DataFrame: + items = await self._execute(SymbolBelongBoardCmd(market, code)) + return _to_df(items) + + # ------------------------------------------------------------------ # + # 资金流向 + # ------------------------------------------------------------------ # + + async def get_capital_flow(self, market: int, code: str) -> pd.DataFrame: + data = await self._execute(SymbolCapitalFlowCmd(market, code)) + if data is None: + return pd.DataFrame() + return _to_df(data) + + # ------------------------------------------------------------------ # + # 集合竞价 + # ------------------------------------------------------------------ # + + async def get_auction(self, market: int, code: str) -> pd.DataFrame: + items = await self._execute(SymbolAuctionCmd(market, code)) + return _to_df(items) + + # ------------------------------------------------------------------ # + # 异动 + # ------------------------------------------------------------------ # + + async def get_unusual( + self, + market: int, + start: int = 0, + count: int = 0, + ) -> pd.DataFrame: + items = await self._execute(UnusualCmd(market, start, count or 600)) + return _to_df(items) + + # ------------------------------------------------------------------ # + # 服务器信息 + # ------------------------------------------------------------------ # + + async def get_server_info(self) -> pd.DataFrame: + info = await self._execute(ServerInfoCmd()) + return _to_df(info) + + async def get_kline_offset( + self, + offset: int = 0, + count: int = 128000, + ) -> pd.DataFrame: + info = await self._execute(KlineOffsetCmd(offset, count)) + return _to_df(info) + + # ------------------------------------------------------------------ # + # 文件操作 + # ------------------------------------------------------------------ # + + async def get_file_meta(self, filename: str) -> pd.DataFrame: + meta = await self._execute(FileListCmd(filename)) + return _to_df(meta) + + async def download_file_chunk( + self, + filename: str, + index: int, + offset: int, + size: int, + ) -> bytes: + return await self._execute(FileDownloadCmd(filename, index, offset, size)) + + async def download_file( + self, + filename: str, + filesize: int = 0, + ) -> bytearray: + if filesize <= 0: + meta = await self._execute(FileListCmd(filename)) + filesize = meta.size + + full_data = bytearray() + chunk_size = 30000 + pos = 0 + idx = 1 + + while pos < filesize: + chunk = await self._execute(FileDownloadCmd(filename, idx, pos, chunk_size)) + if not chunk: + break + full_data.extend(chunk) + pos += len(chunk) + idx += 1 + if len(chunk) < chunk_size: + break + + return full_data + + # ------------------------------------------------------------------ # + # 扩展市场 + # ------------------------------------------------------------------ # + + async def get_goods_list( + self, + market: int, + start: int = 0, + count: int = 600, + ) -> pd.DataFrame: + items = await self._execute(GoodsListCmd(market, start, count)) + return _to_df(items) diff --git a/src/easy_tdx/mac/commands/__init__.py b/src/easy_tdx/mac/commands/__init__.py new file mode 100644 index 0000000..b067bce --- /dev/null +++ b/src/easy_tdx/mac/commands/__init__.py @@ -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", +] diff --git a/src/easy_tdx/mac/commands/board_list.py b/src/easy_tdx/mac/commands/board_list.py new file mode 100644 index 0000000..f73afac --- /dev/null +++ b/src/easy_tdx/mac/commands/board_list.py @@ -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 = " None: + self._board_type = board_type + self._start = start + self._page_size = page_size + + def build_request(self) -> bytes: + # list[BoardInfo]: + count_all, total = unpack_from(" 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( + " list[MacQuoteField]: + # 响应位图(20 字节) + resp_bitmap = body[:20] + + total, row_count = unpack_from(" 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(" list[float]: + if len(body) < _RESPONSE_HEADER_SIZE: + return [] + require_bytes(body, 0, _RESPONSE_HEADER_SIZE, "ChartSamplingCmd header") + (count,) = unpack_from(" 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(" FileMeta: + require_bytes(body, 0, 4 + 4 + 1 + 32, "FileListCmd") + offset, size, flag = unpack_from(" 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(" bytes: + if len(body) < 8: + return b"" + return body[8:] diff --git a/src/easy_tdx/mac/commands/goods_list.py b/src/easy_tdx/mac/commands/goods_list.py new file mode 100644 index 0000000..12e3a35 --- /dev/null +++ b/src/easy_tdx/mac/commands/goods_list.py @@ -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 = " 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(" list[GoodsItem]: + require_bytes(body, 0, 2, "GoodsListCmd header") + (total,) = unpack_from(" None: + self._offset = offset + self._count = count + + def build_request(self) -> bytes: + # I:offset, I:count, 5 bytes padding + body = struct.pack(" KlineOffsetInfo: + if len(body) < 8: + return KlineOffsetInfo(total=0, returned=0) + + # total 字段为大端序! + total = struct.unpack(">I", body[:4])[0] + returned = struct.unpack(" 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(" tuple[str, int]: + d = unpack_from(" 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(" 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( + " list[AuctionItem]: + # 响应头: H:market, 22s:code, I:count, 8 bytes padding (zeros) + _market, _code, count = unpack_from(" len(body): + break + time_sec, price, matched, unmatched = unpack_from( + " 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( + " list[MacBar]: + # 头部: market(2) + code(22) + category(2) + flag(1) + count(2) + start(4) = 33 + (category_flag, _flag, count, start) = unpack_from(" len(body): + break + (ymd, time_num, open_, high, low, close, amount, vol, float_shares) = unpack_from( + " 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 diff --git a/src/easy_tdx/mac/commands/symbol_belong_board.py b/src/easy_tdx/mac/commands/symbol_belong_board.py new file mode 100644 index 0000000..e4eb83d --- /dev/null +++ b/src/easy_tdx/mac/commands/symbol_belong_board.py @@ -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( + " 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 diff --git a/src/easy_tdx/mac/commands/symbol_capital_flow.py b/src/easy_tdx/mac/commands/symbol_capital_flow.py new file mode 100644 index 0000000..755d1ad --- /dev/null +++ b/src/easy_tdx/mac/commands/symbol_capital_flow.py @@ -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( + " 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, + ) diff --git a/src/easy_tdx/mac/commands/symbol_info.py b/src/easy_tdx/mac/commands/symbol_info.py new file mode 100644 index 0000000..c884d45 --- /dev/null +++ b/src/easy_tdx/mac/commands/symbol_info.py @@ -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(" MacSymbolInfo: + # data[0:8] padding (zeros) + # data[8:74] market(2) + code(22) + name(44) + (market, code_raw, name_raw) = unpack_from(" 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(" list[MacQuoteField]: + pos = 0 + field_bitmap = body[pos : pos + 20] + pos += 20 + + (total_stocks, row_count) = unpack_from(" len(body): + break + row_data = body[pos:row_end] + pos = row_end + + (market, code_raw, name_raw) = unpack_from(" 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( + " 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( + " 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( + " list[MacTransaction]: + # 头部: market(2) + code(22) + query_date(4) + flag(1) + count(2) + start(4) + total(4) = 39 + (count,) = unpack_from(" None: + self._market = market + self._code = code + if start_date is not None: + self._start_ymd = start_date.year * 10000 + start_date.month * 100 + start_date.day + else: + self._start_ymd = 0 + self._days = days + + def build_request(self) -> bytes: + body = struct.pack( + " MacMultiTickChart: + # 头部 + (market, code_raw) = unpack_from(" tuple[str, str]: + """根据异动类型解析描述和数值。""" + if len(data) < 13: + return "", "" + v1, v2, v3, v4 = struct.unpack_from("= 10: + sub_type, v2_alt, v3_alt = struct.unpack_from(" None: + self._market = market + self._start = start + self._count = min(count, 600) + + def build_request(self) -> bytes: + # H:market, H:start, 2x padding, H:count, 2x padding, 5×H monitoring params + body = struct.pack( + " list[UnusualItem]: + (count,) = unpack_from(" len(body): + break + + market, code_raw, _, unusual_type, _, index, _z = unpack_from( + " 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._reader: asyncio.StreamReader | None = None self._writer: asyncio.StreamWriter | None = None # 单连接不支持请求复用;所有 IO 在连接内串行执行。 diff --git a/src/easy_tdx/transport/sync.py b/src/easy_tdx/transport/sync.py index bdb28f8..4a82ddb 100644 --- a/src/easy_tdx/transport/sync.py +++ b/src/easy_tdx/transport/sync.py @@ -1,12 +1,14 @@ """同步 TCP 连接(基于 socket)。""" 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 ..commands.setup import SETUP_COMMANDS +from ..config import get_best_host, get_calc_hosts, get_known_hosts, get_mac_hosts, get_port, get_timeout from ..exceptions import TdxConnectionError if TYPE_CHECKING: @@ -14,83 +16,27 @@ if TYPE_CHECKING: T = TypeVar("T") -_DEFAULT_HOST = "180.153.18.170" -_DEFAULT_PORT = 7709 -_DEFAULT_TIMEOUT = 15.0 +_DEFAULT_HEARTBEAT_INTERVAL = 15.0 +_MAX_CONSECUTIVE_HEARTBEATS = 20 -# 已知可用的通达信行情服务器(按优先级排序) -# 原有地址 -KNOWN_HOSTS: list[str] = [ - "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", - # 来自通达信 connect.cfg [HQHOST](2025-05) - "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", - "150.158.160.2", - "123.60.164.122", - "111.229.247.189", - "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", -] - -# 计算服务器(用于下载 tdxfin/ 财务数据) -CALC_HOSTS: list[str] = [ - "120.76.152.87", -] +# 模块级别名,供外部 `from easy_tdx.transport.sync import KNOWN_HOSTS` 使用。 +# 在 import 时从配置读取一次;用户修改 config.json 后需重启生效。 +KNOWN_HOSTS = get_known_hosts() +CALC_HOSTS = get_calc_hosts() +MAC_HOSTS = get_mac_hosts() def ping_host( host: str, - port: int = _DEFAULT_PORT, + port: int | None = None, timeout: float = 5.0, ) -> float | None: """测量连接到指定服务器并完成握手所需的时间(秒)。 返回延迟(秒),连接失败时返回 None。 """ + if port is None: + port = get_port() t0 = time.monotonic() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) @@ -113,14 +59,18 @@ def ping_host( 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, latency_seconds) 列表。 不可达的服务器不包含在结果中。 """ + if hosts is None: + hosts = get_known_hosts() + if port is None: + port = get_port() import concurrent.futures results: list[tuple[str, float]] = [] @@ -135,6 +85,17 @@ def ping_all( return results +def ping_mac_all( + hosts: list[str] | None = None, + port: int | None = None, + timeout: float = 5.0, +) -> list[tuple[str, float]]: + """并发测量多台 MAC 服务器延迟,返回按延迟排序的 (host, latency_seconds) 列表。""" + if hosts is None: + hosts = get_mac_hosts() + return ping_all(hosts=hosts, port=port, timeout=timeout) + + def _recv_exact_sock(sock: socket.socket, n: int) -> bytes: buf = bytearray() while len(buf) < n: @@ -156,14 +117,20 @@ class TdxConnection: def __init__( self, - host: str = _DEFAULT_HOST, - port: int = _DEFAULT_PORT, - timeout: float = _DEFAULT_TIMEOUT, + host: str | None = None, + port: int | None = None, + timeout: float | None = None, ) -> 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._sock: socket.socket | None = None + self._lock = threading.Lock() + self._heartbeat_interval: float = 0 # 0 = disabled + self._stop_event: threading.Event | None = None + self._heartbeat_thread: threading.Thread | None = None + self._last_active: float = 0.0 + self._consecutive_heartbeats: int = 0 def connect(self) -> None: """建立 TCP 连接并完成握手(发送3条 setup 命令)。""" @@ -187,6 +154,7 @@ class TdxConnection: def close(self) -> None: """关闭连接。""" + self.stop_heartbeat() if self._sock is not None: try: self._sock.close() @@ -196,18 +164,21 @@ class TdxConnection: 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: + self._last_active = time.monotonic() + self._consecutive_heartbeats = 0 + 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) # ------------------------------------------------------------------ # # context manager @@ -225,6 +196,66 @@ class TdxConnection: ) -> None: self.close() + # ------------------------------------------------------------------ # + # heartbeat + # ------------------------------------------------------------------ # + + def start_heartbeat(self, interval: float = _DEFAULT_HEARTBEAT_INTERVAL) -> None: + """启动心跳守护线程,定期发送 setup 包保活。""" + self._heartbeat_interval = interval + self._last_active = time.monotonic() + self._stop_event = threading.Event() + self._heartbeat_thread = threading.Thread( + target=self._heartbeat_loop, + daemon=True, + name="tdx-heartbeat", + ) + self._heartbeat_thread.start() + + def stop_heartbeat(self) -> None: + """停止心跳线程。""" + stop_event = self._stop_event + thread = self._heartbeat_thread + if stop_event is not None: + stop_event.set() + if thread is not None: + thread.join(timeout=2.0) + self._stop_event = None + self._heartbeat_thread = None + self._heartbeat_interval = 0 + + def _heartbeat_loop(self) -> None: + """心跳循环:在后台线程中运行。""" + assert self._stop_event is not None + interval = self._heartbeat_interval + while not self._stop_event.wait(timeout=interval): + if time.monotonic() - self._last_active <= interval: + continue + with self._lock: + if self._sock is None: + return + self._consecutive_heartbeats += 1 + if self._consecutive_heartbeats >= _MAX_CONSECUTIVE_HEARTBEATS: + try: + self._sock.close() + except OSError: + pass + self._sock = None + return + try: + self._sock.sendall(SETUP_COMMANDS[0]) + hdr_buf = _recv_exact_sock(self._sock, HEADER_SIZE) + hdr = parse_header(hdr_buf) + if hdr.zipsize > 0: + _recv_exact_sock(self._sock, hdr.zipsize) + except OSError: + try: + self._sock.close() + except OSError: + pass + self._sock = None + return + # ------------------------------------------------------------------ # # internals # ------------------------------------------------------------------ # diff --git a/src/easy_tdx/unified.py b/src/easy_tdx/unified.py new file mode 100644 index 0000000..af18a97 --- /dev/null +++ b/src/easy_tdx/unified.py @@ -0,0 +1,589 @@ +"""统一通达信客户端 -- 自动路由 A 股 / 扩展市场。""" + +from __future__ import annotations + +from types import TracebackType +from typing import Any + +import pandas as pd + +from .ex.mac_client import AsyncMacExClient, MacExClient +from .mac.client import AsyncMacClient, MacClient +from .mac.enums import ( + Adjust, + BoardType, + Category, + FilterType, + Period, + SortOrder, + SortType, +) + + +class UnifiedTdxClient: + """统一通达信行情客户端。 + + 自动路由:A 股方法代理到 MacClient,扩展市场方法代理到 MacExClient。 + MacClient 在 connect()/__enter__ 时立即连接;MacExClient 延迟到首次使用。 + + 用法:: + + with UnifiedTdxClient() as client: + df = client.get_stock_kline(0, "600000", Period.DAILY, count=10) + df2 = client.goods_kline(ExMarket.US_STOCK, "TSLA", Period.DAILY, count=10) + """ + + def __init__( + self, + heartbeat_interval: float = 15.0, + timeout: float = 15.0, + ) -> None: + self._heartbeat_interval = heartbeat_interval + self._timeout = timeout + self._mac: MacClient | None = None + self._mac_ex: MacExClient | None = None + + def connect(self) -> None: + self._ensure_mac() + + def close(self) -> None: + if self._mac is not None: + self._mac.close() + self._mac = None + if self._mac_ex is not None: + self._mac_ex.close() + self._mac_ex = None + + def disconnect(self) -> None: + self.close() + + def __enter__(self) -> UnifiedTdxClient: + self.connect() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + # ------------------------------------------------------------------ # + # 内部路由 + # ------------------------------------------------------------------ # + + def _ensure_mac(self) -> MacClient: + if self._mac is None: + self._mac = MacClient.from_best_host( + heartbeat_interval=self._heartbeat_interval, + timeout=self._timeout, + ) + self._mac.connect() + return self._mac + + def _ensure_mac_ex(self) -> MacExClient: + if self._mac_ex is None: + self._mac_ex = MacExClient.from_best_host(timeout=self._timeout) + self._mac_ex.connect() + return self._mac_ex + + # ------------------------------------------------------------------ # + # A 股方法 (proxy to MacClient) + # ------------------------------------------------------------------ # + + def get_stock_quotes( + self, + stocks: list[tuple[int, str]], + fields: object = None, + ) -> pd.DataFrame: + return self._ensure_mac().get_stock_quotes(stocks, fields) + + def get_stock_quotes_list( + self, + category: Category, + start: int = 0, + count: int = 80, + sort_type: SortType = SortType.CHANGE_PCT, + sort_order: SortOrder = SortOrder.DESC, + exclude_flags: list[FilterType] | None = None, + fields: object = None, + ) -> pd.DataFrame: + return self._ensure_mac().get_stock_quotes_list( + category, start, count, sort_type, sort_order, exclude_flags, fields + ) + + def get_stock_kline( + self, + market: int, + code: str, + period: Period = Period.DAILY, + start: int = 0, + count: int = 800, + times: int = 1, + adjust: Adjust = Adjust.NONE, + ) -> pd.DataFrame: + return self._ensure_mac().get_stock_kline(market, code, period, start, count, times, adjust) + + def get_tick_chart( + self, + market: int, + code: str, + date: int | None = None, + ) -> pd.DataFrame: + return self._ensure_mac().get_tick_chart(market, code, date) + + def get_tick_charts( + self, + market: int, + code: str, + date: int | None = None, + days: int = 5, + ) -> pd.DataFrame: + return self._ensure_mac().get_tick_charts(market, code, date, days) + + def get_chart_sampling(self, market: int, code: str) -> pd.DataFrame: + return self._ensure_mac().get_chart_sampling(market, code) + + def get_transactions( + self, + market: int, + code: str, + count: int = 2000, + start: int = 0, + date: int | None = None, + ) -> pd.DataFrame: + return self._ensure_mac().get_transactions(market, code, count, start, date) + + def get_symbol_info(self, market: int, code: str) -> pd.DataFrame: + return self._ensure_mac().get_symbol_info(market, code) + + def get_board_list( + self, + board_type: BoardType = BoardType.ALL, + count: int = 10000, + ) -> pd.DataFrame: + return self._ensure_mac().get_board_list(board_type, count) + + def get_board_members( + self, + board_symbol: str, + count: int = 100000, + sort_type: SortType = SortType.CHANGE_PCT, + sort_order: SortOrder = SortOrder.DESC, + fields: object = None, + exclude_flags: list[FilterType] | None = None, + ) -> pd.DataFrame: + return self._ensure_mac().get_board_members( + board_symbol, count, sort_type, sort_order, fields, exclude_flags + ) + + def get_belong_board(self, market: int, code: str) -> pd.DataFrame: + return self._ensure_mac().get_belong_board(market, code) + + def get_capital_flow(self, market: int, code: str) -> pd.DataFrame: + return self._ensure_mac().get_capital_flow(market, code) + + def get_auction(self, market: int, code: str) -> pd.DataFrame: + return self._ensure_mac().get_auction(market, code) + + def get_unusual( + self, + market: int, + start: int = 0, + count: int = 0, + ) -> pd.DataFrame: + return self._ensure_mac().get_unusual(market, start, count) + + def get_server_info(self) -> pd.DataFrame: + return self._ensure_mac().get_server_info() + + def get_kline_offset( + self, + offset: int = 0, + count: int = 128000, + ) -> pd.DataFrame: + return self._ensure_mac().get_kline_offset(offset, count) + + def get_file_meta(self, filename: str) -> pd.DataFrame: + return self._ensure_mac().get_file_meta(filename) + + def download_file_chunk( + self, + filename: str, + index: int, + offset: int, + size: int, + ) -> bytes: + return self._ensure_mac().download_file_chunk(filename, index, offset, size) + + def download_file( + self, + filename: str, + filesize: int = 0, + ) -> bytearray: + return self._ensure_mac().download_file(filename, filesize) + + def get_goods_list( + self, + market: int, + start: int = 0, + count: int = 600, + ) -> pd.DataFrame: + return self._ensure_mac_ex().goods_list(market, start, count) + + # ------------------------------------------------------------------ # + # 扩展市场方法 (proxy to MacExClient) + # ------------------------------------------------------------------ # + + def goods_count(self, market: int) -> int: + return self._ensure_mac_ex().goods_count(market) + + def goods_list(self, market: int, start: int = 0, count: int = 600) -> pd.DataFrame: + return self._ensure_mac_ex().goods_list(market, start, count) + + def goods_quotes( + self, + stocks: list[tuple[int, str]], + fields: Any = None, + ) -> pd.DataFrame: + return self._ensure_mac_ex().goods_quotes(stocks, fields) + + 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: + return self._ensure_mac_ex().goods_quotes_list(market, start, count, sort_type, sort_order) + + def goods_kline( + self, + market: int, + code: str, + period: Period = Period.DAILY, + start: int = 0, + count: int = 800, + adjust: Adjust = Adjust.NONE, + ) -> pd.DataFrame: + return self._ensure_mac_ex().goods_kline(market, code, period, start, count, adjust) + + def goods_tick_chart( + self, + market: int, + code: str, + query_date: object = None, + ) -> pd.DataFrame: + return self._ensure_mac_ex().goods_tick_chart(market, code, query_date) # type: ignore[arg-type] + + def goods_chart_sampling(self, market: int, code: str) -> pd.DataFrame: + return self._ensure_mac_ex().goods_chart_sampling(market, code) + + def goods_transaction( + self, + market: int, + code: str, + query_date: object = None, + start: int = 0, + count: int = 2000, + ) -> pd.DataFrame: + return self._ensure_mac_ex().goods_transaction(market, code, query_date, start, count) # type: ignore[arg-type] + + +class AsyncUnifiedTdxClient: + """异步统一通达信行情客户端。 + + 用法:: + + async with AsyncUnifiedTdxClient() as client: + df = await client.get_stock_kline(0, "600000", Period.DAILY, count=10) + df2 = await client.goods_kline(ExMarket.US_STOCK, "TSLA", Period.DAILY, count=10) + """ + + def __init__( + self, + heartbeat_interval: float = 15.0, + timeout: float = 15.0, + ) -> None: + self._heartbeat_interval = heartbeat_interval + self._timeout = timeout + self._mac: AsyncMacClient | None = None + self._mac_ex: AsyncMacExClient | None = None + + async def connect(self) -> None: + await self._ensure_mac() + + async def close(self) -> None: + if self._mac is not None: + await self._mac.close() + self._mac = None + if self._mac_ex is not None: + await self._mac_ex.close() + self._mac_ex = None + + async def disconnect(self) -> None: + await self.close() + + async def __aenter__(self) -> AsyncUnifiedTdxClient: + 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() + + # ------------------------------------------------------------------ # + # 内部路由 + # ------------------------------------------------------------------ # + + async def _ensure_mac(self) -> AsyncMacClient: + if self._mac is None: + self._mac = AsyncMacClient.from_best_host( + heartbeat_interval=self._heartbeat_interval, + timeout=self._timeout, + ) + await self._mac.connect() + return self._mac + + async def _ensure_mac_ex(self) -> AsyncMacExClient: + if self._mac_ex is None: + self._mac_ex = AsyncMacExClient.from_best_host(timeout=self._timeout) + await self._mac_ex.connect() + return self._mac_ex + + # ------------------------------------------------------------------ # + # A 股方法 (proxy to AsyncMacClient) + # ------------------------------------------------------------------ # + + async def get_stock_quotes( + self, + stocks: list[tuple[int, str]], + fields: object = None, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_stock_quotes(stocks, fields) + + async def get_stock_quotes_list( + self, + category: Category, + start: int = 0, + count: int = 80, + sort_type: SortType = SortType.CHANGE_PCT, + sort_order: SortOrder = SortOrder.DESC, + exclude_flags: list[FilterType] | None = None, + fields: object = None, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_stock_quotes_list( + category, start, count, sort_type, sort_order, exclude_flags, fields + ) + + async def get_stock_kline( + self, + market: int, + code: str, + period: Period = Period.DAILY, + start: int = 0, + count: int = 800, + times: int = 1, + adjust: Adjust = Adjust.NONE, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_stock_kline(market, code, period, start, count, times, adjust) + + async def get_tick_chart( + self, + market: int, + code: str, + date: int | None = None, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_tick_chart(market, code, date) + + async def get_tick_charts( + self, + market: int, + code: str, + date: int | None = None, + days: int = 5, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_tick_charts(market, code, date, days) + + async def get_chart_sampling(self, market: int, code: str) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_chart_sampling(market, code) + + async def get_transactions( + self, + market: int, + code: str, + count: int = 2000, + start: int = 0, + date: int | None = None, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_transactions(market, code, count, start, date) + + async def get_symbol_info(self, market: int, code: str) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_symbol_info(market, code) + + async def get_board_list( + self, + board_type: BoardType = BoardType.ALL, + count: int = 10000, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_board_list(board_type, count) + + async def get_board_members( + self, + board_symbol: str, + count: int = 100000, + sort_type: SortType = SortType.CHANGE_PCT, + sort_order: SortOrder = SortOrder.DESC, + fields: object = None, + exclude_flags: list[FilterType] | None = None, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_board_members( + board_symbol, count, sort_type, sort_order, fields, exclude_flags + ) + + async def get_belong_board(self, market: int, code: str) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_belong_board(market, code) + + async def get_capital_flow(self, market: int, code: str) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_capital_flow(market, code) + + async def get_auction(self, market: int, code: str) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_auction(market, code) + + async def get_unusual( + self, + market: int, + start: int = 0, + count: int = 0, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_unusual(market, start, count) + + async def get_server_info(self) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_server_info() + + async def get_kline_offset( + self, + offset: int = 0, + count: int = 128000, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_kline_offset(offset, count) + + async def get_file_meta(self, filename: str) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_file_meta(filename) + + async def download_file_chunk( + self, + filename: str, + index: int, + offset: int, + size: int, + ) -> bytes: + mac = await self._ensure_mac() + return await mac.download_file_chunk(filename, index, offset, size) + + async def download_file( + self, + filename: str, + filesize: int = 0, + ) -> bytearray: + mac = await self._ensure_mac() + return await mac.download_file(filename, filesize) + + async def get_goods_list( + self, + market: int, + start: int = 0, + count: int = 600, + ) -> pd.DataFrame: + ex = await self._ensure_mac_ex() + return await ex.goods_list(market, start, count) + + # ------------------------------------------------------------------ # + # 扩展市场方法 (proxy to AsyncMacExClient) + # ------------------------------------------------------------------ # + + async def goods_count(self, market: int) -> int: + ex = await self._ensure_mac_ex() + return await ex.goods_count(market) + + async def goods_list(self, market: int, start: int = 0, count: int = 600) -> pd.DataFrame: + ex = await self._ensure_mac_ex() + return await ex.goods_list(market, start, count) + + async def goods_quotes( + self, + stocks: list[tuple[int, str]], + fields: Any = None, + ) -> pd.DataFrame: + ex = await self._ensure_mac_ex() + return await ex.goods_quotes(stocks, fields) + + 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: + ex = await self._ensure_mac_ex() + return await ex.goods_quotes_list(market, start, count, sort_type, sort_order) + + 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: + ex = await self._ensure_mac_ex() + return await ex.goods_kline(market, code, period, start, count, adjust) + + async def goods_tick_chart( + self, + market: int, + code: str, + query_date: object = None, + ) -> pd.DataFrame: + ex = await self._ensure_mac_ex() + return await ex.goods_tick_chart(market, code, query_date) # type: ignore[arg-type] + + async def goods_chart_sampling(self, market: int, code: str) -> pd.DataFrame: + ex = await self._ensure_mac_ex() + return await ex.goods_chart_sampling(market, code) + + async def goods_transaction( + self, + market: int, + code: str, + query_date: object = None, + start: int = 0, + count: int = 2000, + ) -> pd.DataFrame: + ex = await self._ensure_mac_ex() + return await ex.goods_transaction(market, code, query_date, start, count) # type: ignore[arg-type] diff --git a/tests/unit/test_async_transport.py b/tests/unit/test_async_transport.py index 31eca18..559872c 100644 --- a/tests/unit/test_async_transport.py +++ b/tests/unit/test_async_transport.py @@ -119,7 +119,7 @@ def test_async_client_request_timeout() -> None: server = await asyncio.start_server(handle, "127.0.0.1", 0) port = server.sockets[0].getsockname()[1] try: - client = AsyncTdxClient("127.0.0.1", port=port, timeout=0.05) + client = AsyncTdxClient("127.0.0.1", port=port, timeout=0.05, auto_reconnect=False) await client.connect() t0 = time.monotonic() try: