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 <noreply@anthropic.com>
This commit is contained in:
GitHub
2026-05-26 22:46:52 +08:00
co-authored by Claude Opus 4.7
parent 2ec36e01bf
commit 72652b9f8b
6 changed files with 210 additions and 15 deletions
+6
View File
@@ -222,6 +222,11 @@ with MacClient.from_best_host() as c:
# "down_count": 37, # "down_count": 37,
# "members": DataFrame(...), # 成分股明细 # "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_list(board_type, ...)` | 板块列表 |
| `get_board_members(board_symbol, ...)` | 板块成分股报价 | | `get_board_members(board_symbol, ...)` | 板块成分股报价 |
| `get_board_summary(board_symbol, ...)` | 板块汇总(成交额、主力净流入、涨跌家数) | | `get_board_summary(board_symbol, ...)` | 板块汇总(成交额、主力净流入、涨跌家数) |
| `get_board_ranking(board_type, top_n, sort_by, ...)` | 板块涨跌幅排行榜(行业/概念排行) |
| `get_belong_board(market, code)` | 个股所属板块 | | `get_belong_board(market, code)` | 个股所属板块 |
| `get_capital_flow(market, code)` | 资金流向 | | `get_capital_flow(market, code)` | 资金流向 |
| `get_auction(market, code)` | 集合竞价 | | `get_auction(market, code)` | 集合竞价 |
+48
View File
@@ -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 ...
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "easy-tdx" name = "easy-tdx"
version = "1.2.1" version = "1.3.0"
description = "通达信 TCP 协议行情数据客户端,支持在线行情与离线本地数据读取" description = "通达信 TCP 协议行情数据客户端,支持在线行情与离线本地数据读取"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
+1 -1
View File
@@ -107,4 +107,4 @@ __all__ = [
"save_best_ex_host", "save_best_ex_host",
] ]
__version__ = "1.0.0" __version__ = "1.3.0"
+1 -1
View File
@@ -19,7 +19,7 @@ from .cmd_transaction import transaction
@click.group() @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: def cli() -> None:
"""easy-tdx -- 通达信行情数据 CLI(默认 JSON 输出,适合 Agent 使用)。 """easy-tdx -- 通达信行情数据 CLI(默认 JSON 输出,适合 Agent 使用)。
+153 -12
View File
@@ -76,6 +76,8 @@ def _convert_board_code(board_symbol: str) -> int:
if s.startswith("000"): if s.startswith("000"):
return 31000 + int(s) return 31000 + int(s)
return int(s) return int(s)
_TRANSACTION_PAGE_SIZE = 1000 _TRANSACTION_PAGE_SIZE = 1000
_T = TypeVar("_T") _T = TypeVar("_T")
@@ -393,8 +395,7 @@ class MacClient:
from datetime import date as date_cls from datetime import date as date_cls
query_date = ( query_date = (
date_cls(date // 10000, (date % 10000) // 100, date % 100) date_cls(date // 10000, (date % 10000) // 100, date % 100) if date is not None else None
if date is not None else None
) )
chart = self._execute(SymbolTickChartCmd(market, code, query_date)) chart = self._execute(SymbolTickChartCmd(market, code, query_date))
return pd.DataFrame(_flatten_tick_chart(chart)) return pd.DataFrame(_flatten_tick_chart(chart))
@@ -417,8 +418,7 @@ class MacClient:
from datetime import date as date_cls from datetime import date as date_cls
start_date = ( start_date = (
date_cls(date // 10000, (date % 10000) // 100, date % 100) date_cls(date // 10000, (date % 10000) // 100, date % 100) if date is not None else None
if date is not None else None
) )
chart = self._execute(TickChartsCmd(market, code, start_date, days)) chart = self._execute(TickChartsCmd(market, code, start_date, days))
return pd.DataFrame(_flatten_multi_tick_chart(chart)) return pd.DataFrame(_flatten_multi_tick_chart(chart))
@@ -457,8 +457,7 @@ class MacClient:
from datetime import date as date_cls from datetime import date as date_cls
query_date = ( query_date = (
date_cls(date // 10000, (date % 10000) // 100, date % 100) date_cls(date // 10000, (date % 10000) // 100, date % 100) if date is not None else None
if date is not None else None
) )
all_items = self._execute( all_items = self._execute(
SymbolTransactionCmd( SymbolTransactionCmd(
@@ -653,6 +652,80 @@ class MacClient:
"members": df, "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 from datetime import date as date_cls
query_date = ( query_date = (
date_cls(date // 10000, (date % 10000) // 100, date % 100) date_cls(date // 10000, (date % 10000) // 100, date % 100) if date is not None else None
if date is not None else None
) )
chart = await self._execute(SymbolTickChartCmd(market, code, query_date)) chart = await self._execute(SymbolTickChartCmd(market, code, query_date))
return pd.DataFrame(_flatten_tick_chart(chart)) return pd.DataFrame(_flatten_tick_chart(chart))
@@ -1096,8 +1168,7 @@ class AsyncMacClient:
from datetime import date as date_cls from datetime import date as date_cls
start_date = ( start_date = (
date_cls(date // 10000, (date % 10000) // 100, date % 100) date_cls(date // 10000, (date % 10000) // 100, date % 100) if date is not None else None
if date is not None else None
) )
chart = await self._execute(TickChartsCmd(market, code, start_date, days)) chart = await self._execute(TickChartsCmd(market, code, start_date, days))
return pd.DataFrame(_flatten_multi_tick_chart(chart)) return pd.DataFrame(_flatten_multi_tick_chart(chart))
@@ -1121,8 +1192,7 @@ class AsyncMacClient:
from datetime import date as date_cls from datetime import date as date_cls
query_date = ( query_date = (
date_cls(date // 10000, (date % 10000) // 100, date % 100) date_cls(date // 10000, (date % 10000) // 100, date % 100) if date is not None else None
if date is not None else None
) )
all_items = await self._execute( all_items = await self._execute(
SymbolTransactionCmd( SymbolTransactionCmd(
@@ -1291,6 +1361,77 @@ class AsyncMacClient:
"members": df, "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
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 资金流向 # 资金流向
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #