From 72652b9f8b8cd41bd27367a1898b92a3dfb6acc3 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 26 May 2026 22:46:52 +0800 Subject: [PATCH] feat: add get_board_ranking() for board change-pct ranking New MacClient/AsyncMacClient method that ranks all boards of a given type (industry/concept) by change_pct, amount, main_net_amount, or vol. Aggregates member quotes via get_board_summary() for each board. Also bumps version to 1.3.0 and updates README + CLI version. Co-Authored-By: Claude Opus 4.7 --- README.md | 6 + examples/15_mac_board/board_ranking.py | 48 +++++++ pyproject.toml | 2 +- src/easy_tdx/__init__.py | 2 +- src/easy_tdx/cli/__init__.py | 2 +- src/easy_tdx/mac/client.py | 165 +++++++++++++++++++++++-- 6 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 examples/15_mac_board/board_ranking.py diff --git a/README.md b/README.md index d2e7d0c..4961701 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,11 @@ with MacClient.from_best_host() as c: # "down_count": 37, # "members": DataFrame(...), # 成分股明细 # } + + # 板块涨跌幅排行榜 + df = c.get_board_ranking(BoardType.HY, top_n=10, sort_by="change_pct") + df = c.get_board_ranking(BoardType.GN, top_n=20, sort_by="main_net_amount") + # 返回列:code, name, change_pct, amount, vol, main_net_amount, up_count, down_count, member_count ``` #### 资金流向 @@ -404,6 +409,7 @@ bars = read_daily_bars(filepath) | `get_board_list(board_type, ...)` | 板块列表 | | `get_board_members(board_symbol, ...)` | 板块成分股报价 | | `get_board_summary(board_symbol, ...)` | 板块汇总(成交额、主力净流入、涨跌家数) | +| `get_board_ranking(board_type, top_n, sort_by, ...)` | 板块涨跌幅排行榜(行业/概念排行) | | `get_belong_board(market, code)` | 个股所属板块 | | `get_capital_flow(market, code)` | 资金流向 | | `get_auction(market, code)` | 集合竞价 | diff --git a/examples/15_mac_board/board_ranking.py b/examples/15_mac_board/board_ranking.py new file mode 100644 index 0000000..6671c6f --- /dev/null +++ b/examples/15_mac_board/board_ranking.py @@ -0,0 +1,48 @@ +"""演示:板块涨跌幅排行榜。 + +通过 MacClient 的 get_board_ranking() 获取行业或概念板块的聚合排行数据, +包含涨跌幅、成交额、成交量、主力净流入、涨跌家数等。 + +board_type 参数: + BoardType.HY — 行业板块 + BoardType.GN — 概念板块 + +返回 DataFrame 列: + code 板块代码 + name 板块名称 + change_pct 涨跌幅% + amount 板块总成交额(元) + vol 板块总成交量(股) + main_net_amount 板块主力净流入(元) + up_count 上涨家数 + down_count 下跌家数 + member_count 成分股数量 +""" + +from easy_tdx import MacClient +from easy_tdx.mac.enums import BoardType + +with MacClient.from_best_host() as c: + # 行业板块涨幅 + print("=== 行业板块涨幅 ===") + df_hy = c.get_board_ranking(BoardType.HY, top_n=300, sort_by="change_pct") + print(df_hy.to_string(index=False)) + + print() + + # 概念板块主力净流入 + print("=== 概念板块主力净流入 ===") + df_gn = c.get_board_ranking(BoardType.GN, top_n=300, sort_by="main_net_amount") + print(df_gn.to_string(index=False)) + +# 运行结果示例: +# === 行业板块涨幅 === +# code name change_pct amount vol ... +# 881127 通信设备 3.25 18523456000 1234567890 ... +# 881156 半导体 2.98 25678900000 2345678901 ... +# ... +# +# === 概念板块主力净流入 === +# code name change_pct amount vol ... +# 880952 人工智能 1.56 42345678000 3456789012 ... +# 880930 芯片概念 1.23 38765432000 2987654321 ... diff --git a/pyproject.toml b/pyproject.toml index 9dc278e..1772797 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "easy-tdx" -version = "1.2.1" +version = "1.3.0" description = "通达信 TCP 协议行情数据客户端,支持在线行情与离线本地数据读取" readme = "README.md" requires-python = ">=3.10" diff --git a/src/easy_tdx/__init__.py b/src/easy_tdx/__init__.py index b18db99..713b532 100644 --- a/src/easy_tdx/__init__.py +++ b/src/easy_tdx/__init__.py @@ -107,4 +107,4 @@ __all__ = [ "save_best_ex_host", ] -__version__ = "1.0.0" +__version__ = "1.3.0" diff --git a/src/easy_tdx/cli/__init__.py b/src/easy_tdx/cli/__init__.py index 263fce4..1d0065e 100644 --- a/src/easy_tdx/cli/__init__.py +++ b/src/easy_tdx/cli/__init__.py @@ -19,7 +19,7 @@ from .cmd_transaction import transaction @click.group() -@click.version_option(version="1.1.0", prog_name="easy-tdx") +@click.version_option(version="1.3.0", prog_name="easy-tdx") def cli() -> None: """easy-tdx -- 通达信行情数据 CLI(默认 JSON 输出,适合 Agent 使用)。 diff --git a/src/easy_tdx/mac/client.py b/src/easy_tdx/mac/client.py index d33705d..e91c6f3 100644 --- a/src/easy_tdx/mac/client.py +++ b/src/easy_tdx/mac/client.py @@ -76,6 +76,8 @@ def _convert_board_code(board_symbol: str) -> int: if s.startswith("000"): return 31000 + int(s) return int(s) + + _TRANSACTION_PAGE_SIZE = 1000 _T = TypeVar("_T") @@ -393,8 +395,7 @@ class MacClient: 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 + 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)) @@ -417,8 +418,7 @@ class MacClient: 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 + 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)) @@ -457,8 +457,7 @@ class MacClient: 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 + date_cls(date // 10000, (date % 10000) // 100, date % 100) if date is not None else None ) all_items = self._execute( SymbolTransactionCmd( @@ -653,6 +652,80 @@ class MacClient: "members": df, } + def get_board_ranking( + self, + board_type: BoardType = BoardType.HY, + top_n: int = 50, + sort_by: str = "change_pct", + ascending: bool = False, + ) -> pd.DataFrame: + """获取板块涨跌幅排行榜(含成交额、成交量、资金流入流出、涨跌家数)。 + + 先通过 ``get_board_list`` 获取全部板块,再逐个调用 + ``get_board_summary`` 聚合成分股数据,合并为排行榜 DataFrame。 + + Args: + board_type: 板块类型(``BoardType.HY`` 行业 / ``BoardType.GN`` 概念)。 + top_n: 聚合的板块数量上限。概念板块有 300+ 个, + 全部聚合网络开销大,建议按需限制。 + sort_by: 排序字段,可选 ``change_pct`` / ``amount`` + / ``main_net_amount`` / ``vol``。 + ascending: 排序方向,默认降序。 + + Returns: + DataFrame,列:: + + code 板块代码 + name 板块名称 + change_pct 涨跌幅% + amount 板块总成交额(元) + vol 板块总成交量(股) + main_net_amount 板块主力净流入(元) + up_count 上涨家数 + down_count 下跌家数 + member_count 成分股数量 + """ + _VALID_SORT = {"change_pct", "amount", "main_net_amount", "vol"} + if sort_by not in _VALID_SORT: + raise ValueError(f"sort_by 必须是 {_VALID_SORT} 之一, got {sort_by!r}") + + boards_df = self.get_board_list(board_type) + if boards_df.empty: + return pd.DataFrame() + + # 从 board_list 的 price / pre_close 计算涨跌幅 + if "price" in boards_df.columns and "pre_close" in boards_df.columns: + pre = boards_df["pre_close"].replace(0, float("nan")) + boards_df["change_pct"] = (boards_df["price"] - boards_df["pre_close"]) / pre * 100 + else: + boards_df["change_pct"] = 0.0 + + # 按涨跌幅初排,取 top_n 减少后续聚合开销 + boards_df = boards_df.sort_values("change_pct", ascending=ascending).head(top_n) + + rows: list[dict[str, Any]] = [] + for _, row in boards_df.iterrows(): + code = str(row["code"]) + summary = self.get_board_summary(code) + rows.append( + { + "code": code, + "name": row.get("name", ""), + "change_pct": round(float(row.get("change_pct", 0.0)), 2), + "amount": summary["amount"], + "vol": summary["vol"], + "main_net_amount": summary["main_net_amount"], + "up_count": summary["up_count"], + "down_count": summary["down_count"], + "member_count": summary["member_count"], + } + ) + + result = pd.DataFrame(rows) + if not result.empty: + result = result.sort_values(sort_by, ascending=ascending).reset_index(drop=True) + return result + # ------------------------------------------------------------------ # # 资金流向 # ------------------------------------------------------------------ # @@ -1080,8 +1153,7 @@ class AsyncMacClient: 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 + 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)) @@ -1096,8 +1168,7 @@ class AsyncMacClient: 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 + 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)) @@ -1121,8 +1192,7 @@ class AsyncMacClient: 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 + date_cls(date // 10000, (date % 10000) // 100, date % 100) if date is not None else None ) all_items = await self._execute( SymbolTransactionCmd( @@ -1291,6 +1361,77 @@ class AsyncMacClient: "members": df, } + async def get_board_ranking( + self, + board_type: BoardType = BoardType.HY, + top_n: int = 50, + sort_by: str = "change_pct", + ascending: bool = False, + ) -> pd.DataFrame: + """获取板块涨跌幅排行榜(含成交额、成交量、资金流入流出、涨跌家数)。 + + 先通过 ``get_board_list`` 获取全部板块,再并发调用 + ``get_board_summary`` 聚合成分股数据,合并为排行榜 DataFrame。 + + Args: + board_type: 板块类型(``BoardType.HY`` 行业 / ``BoardType.GN`` 概念)。 + top_n: 聚合的板块数量上限。概念板块有 300+ 个, + 全部聚合网络开销大,建议按需限制。 + sort_by: 排序字段,可选 ``change_pct`` / ``amount`` + / ``main_net_amount`` / ``vol``。 + ascending: 排序方向,默认降序。 + + Returns: + DataFrame,列:: + + code 板块代码 + name 板块名称 + change_pct 涨跌幅% + amount 板块总成交额(元) + vol 板块总成交量(股) + main_net_amount 板块主力净流入(元) + up_count 上涨家数 + down_count 下跌家数 + member_count 成分股数量 + """ + _VALID_SORT = {"change_pct", "amount", "main_net_amount", "vol"} + if sort_by not in _VALID_SORT: + raise ValueError(f"sort_by 必须是 {_VALID_SORT} 之一, got {sort_by!r}") + + boards_df = await self.get_board_list(board_type) + if boards_df.empty: + return pd.DataFrame() + + if "price" in boards_df.columns and "pre_close" in boards_df.columns: + pre = boards_df["pre_close"].replace(0, float("nan")) + boards_df["change_pct"] = (boards_df["price"] - boards_df["pre_close"]) / pre * 100 + else: + boards_df["change_pct"] = 0.0 + + boards_df = boards_df.sort_values("change_pct", ascending=ascending).head(top_n) + + async def _fetch_row(row: pd.Series) -> dict[str, Any]: + code = str(row["code"]) + summary = await self.get_board_summary(code) + return { + "code": code, + "name": row.get("name", ""), + "change_pct": round(float(row.get("change_pct", 0.0)), 2), + "amount": summary["amount"], + "vol": summary["vol"], + "main_net_amount": summary["main_net_amount"], + "up_count": summary["up_count"], + "down_count": summary["down_count"], + "member_count": summary["member_count"], + } + + rows = await asyncio.gather(*[_fetch_row(row) for _, row in boards_df.iterrows()]) + + result = pd.DataFrame(rows) + if not result.empty: + result = result.sort_values(sort_by, ascending=ascending).reset_index(drop=True) + return result + # ------------------------------------------------------------------ # # 资金流向 # ------------------------------------------------------------------ #