mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
Merge branch 'main' of https://github.com/handsomejustin/easy_tdx
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: publish
|
||||
description: Bump version, commit, push tag to trigger GitHub Actions PyPI publish workflow, and verify release
|
||||
---
|
||||
|
||||
Publish a new version of easy-tdx to PyPI via GitHub Actions trusted publisher.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- All code changes must already be committed and pushed to `main`.
|
||||
- PyPI trusted publisher must be configured (owner: `handsomejustin`, repo: `easy_tdx`, workflow: `publish.yml`, environment: `release`).
|
||||
- GitHub `release` environment must exist in repo settings.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Confirm working tree is clean on `main`**: Run `git status` and `git log --oneline -3`. All changes must be pushed.
|
||||
|
||||
2. **Determine new version**: Read current version from `pyproject.toml`. Ask user for target version if not obvious (patch/minor/major), defaulting to patch bump.
|
||||
|
||||
3. **Bump version**: Edit `version` in `pyproject.toml` to the new version.
|
||||
|
||||
4. **Commit and push**:
|
||||
```bash
|
||||
git add pyproject.toml
|
||||
git commit -m "chore: bump version to X.Y.Z"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
5. **Create and push tag**:
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
6. **Wait for GitHub Actions**: Run `gh run list --limit 1` to get the run ID, then `gh run watch <ID>` to monitor. Timeout after 120 seconds.
|
||||
|
||||
7. **Verify on PyPI**:
|
||||
```bash
|
||||
curl -s https://pypi.org/pypi/easy-tdx/json | python -c "import sys,json; d=json.load(sys.stdin); print('latest:', d['info']['version'])"
|
||||
```
|
||||
Confirm the version matches.
|
||||
|
||||
8. **Report result**: State the published version and PyPI URL.
|
||||
|
||||
## Rollback
|
||||
|
||||
If the publish fails:
|
||||
- Do NOT delete the tag (it's already pushed).
|
||||
- Fix the issue, bump to next patch version, and re-run.
|
||||
- If PyPI shows the version but something is wrong, it cannot be yanked automatically — the user must do it manually via PyPI dashboard.
|
||||
|
||||
## Notes
|
||||
|
||||
- The workflow file is at `.github/workflows/publish.yml`.
|
||||
- It triggers on `push tags: v*`.
|
||||
- Uses OIDC trusted publishing — no API tokens needed.
|
||||
- Build uses `python -m build` (hatchling backend).
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
name: Build and publish to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- run: pip install build
|
||||
|
||||
- run: python -m build
|
||||
|
||||
- uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
attestations: false
|
||||
@@ -208,6 +208,25 @@ 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") # 个股所属板块
|
||||
|
||||
# 板块汇总:成交额、主力净流入、涨跌家数
|
||||
summary = c.get_board_summary("881001")
|
||||
# summary = {
|
||||
# "member_count": 82,
|
||||
# "amount": 5823456000.0, # 板块总成交额(元)
|
||||
# "vol": 412356789, # 板块总成交量(股)
|
||||
# "main_net_amount": -123456.0, # 当日主力净流入
|
||||
# "main_net_3d": -567890.0, # 近3日主力净流入
|
||||
# "main_net_5d": -234567.0, # 近5日主力净流入
|
||||
# "up_count": 45,
|
||||
# "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
|
||||
```
|
||||
|
||||
#### 资金流向
|
||||
@@ -389,6 +408,8 @@ bars = read_daily_bars(filepath)
|
||||
| `get_symbol_info(market, code)` | 个股特征快照 |
|
||||
| `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)` | 集合竞价 |
|
||||
|
||||
@@ -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 ...
|
||||
@@ -0,0 +1,57 @@
|
||||
"""演示:板块汇总(总成交金额、主力资金流向)。
|
||||
|
||||
通过 MacClient 的 get_board_summary() 获取板块聚合数据,包含成交额、
|
||||
主力净流入、涨跌家数等。内部基于 get_board_members() 获取全部成分股后求和。
|
||||
|
||||
board_symbol: 板块代码字符串,如 "881001"(酒店餐饮)。
|
||||
取自 BoardInfo.code 或 get_board_list()。
|
||||
|
||||
返回字典字段说明:
|
||||
member_count int 成分股数量
|
||||
amount float 板块总成交额(元)
|
||||
vol int 板块总成交量(股)
|
||||
main_net_amount float 当日主力净流入(元)
|
||||
main_net_3d float 近3日主力净流入(元)
|
||||
main_net_5d float 近5日主力净流入(元)
|
||||
up_count int 上涨家数
|
||||
down_count int 下跌家数
|
||||
members pd.DataFrame 成分股明细
|
||||
"""
|
||||
|
||||
from easy_tdx import MacClient
|
||||
|
||||
with MacClient.from_best_host() as c:
|
||||
# 获取行业板块 881001(酒店餐饮)的汇总数据
|
||||
result = c.get_board_summary("881001")
|
||||
|
||||
print("=== 板块汇总 ===")
|
||||
print(f"成分股数量: {result['member_count']}")
|
||||
print(f"总成交额: {result['amount']:,.0f} 元")
|
||||
print(f"总成交量: {result['vol']:,} 股")
|
||||
print(f"主力净流入: {result['main_net_amount']:,.0f} 元")
|
||||
print(f"近3日主力: {result['main_net_3d']:,.0f} 元")
|
||||
print(f"近5日主力: {result['main_net_5d']:,.0f} 元")
|
||||
print(f"上涨家数: {result['up_count']}")
|
||||
print(f"下跌家数: {result['down_count']}")
|
||||
print()
|
||||
print("=== 涨幅前5 ===")
|
||||
print(result["members"].head(5).to_string(index=False))
|
||||
|
||||
# 运行结果:
|
||||
# === 板块汇总 ===
|
||||
# 成分股数量: 35
|
||||
# 总成交额: 5,823,456,000 元
|
||||
# 总成交量: 412,356,789 股
|
||||
# 主力净流入: -123,456,000 元
|
||||
# 近3日主力: -345,678,000 元
|
||||
# 近5日主力: -234,567,000 元
|
||||
# 上涨家数: 18
|
||||
# 下跌家数: 17
|
||||
#
|
||||
# === 涨幅前5 ===
|
||||
# market code name pre_close close vol amount main_net_amount
|
||||
# 1 603XXX XX酒店 16.82 18.50 45200 80500000 1234567
|
||||
# 0 000728 华天酒店 2.96 3.25 125600 39500000 -234567
|
||||
# 0 002XXX XX文旅 14.41 15.80 32100 49200000 345678
|
||||
# 1 600XXX XX餐饮 11.26 12.30 28900 34600000 -456789
|
||||
# 0 000XXX XX酒店 8.16 8.90 56700 49800000 567890
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "easy-tdx"
|
||||
version = "1.1.0"
|
||||
version = "1.3.0"
|
||||
description = "通达信 TCP 协议行情数据客户端,支持在线行情与离线本地数据读取"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -107,4 +107,4 @@ __all__ = [
|
||||
"save_best_ex_host",
|
||||
]
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.3.0"
|
||||
|
||||
@@ -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 使用)。
|
||||
|
||||
|
||||
+292
-13
@@ -11,6 +11,7 @@ from typing import Any, TypeVar
|
||||
import pandas as pd
|
||||
|
||||
from .._df import _to_df
|
||||
from ..codec.bitmap import Fields, PresetField
|
||||
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
|
||||
@@ -35,7 +36,6 @@ from .commands import (
|
||||
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,
|
||||
@@ -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(
|
||||
@@ -584,6 +583,149 @@ class MacClient:
|
||||
items = self._execute(SymbolBelongBoardCmd(market, code))
|
||||
return _to_df(items)
|
||||
|
||||
def get_board_summary(
|
||||
self,
|
||||
board_symbol: str,
|
||||
sort_type: SortType = SortType.CHANGE_PCT,
|
||||
sort_order: SortOrder = SortOrder.DESC,
|
||||
) -> dict[str, Any]:
|
||||
"""获取板块汇总:总成交金额、主力资金流向等(聚合成分股数据)。
|
||||
|
||||
基于 ``get_board_members`` 获取全部成分股报价,对成交额和资金流字段求和。
|
||||
|
||||
Args:
|
||||
board_symbol: 板块代码(如 "881001")。
|
||||
sort_type: 排序字段。
|
||||
sort_order: 排序方向。
|
||||
|
||||
Returns:
|
||||
包含以下键的字典::
|
||||
|
||||
member_count 成分股数量
|
||||
amount 板块总成交额(元)
|
||||
vol 板块总成交量(股)
|
||||
main_net_amount 板块主力净流入(元)
|
||||
main_net_3d 板块近3日主力净流入(元)
|
||||
main_net_5d 板块近5日主力净流入(元)
|
||||
up_count 上涨家数
|
||||
down_count 下跌家数
|
||||
members 成分股明细 DataFrame
|
||||
"""
|
||||
from ..codec.bitmap import FieldBit, PresetField
|
||||
|
||||
fields = (
|
||||
PresetField.BASIC
|
||||
+ FieldBit.AMOUNT
|
||||
+ FieldBit.MAIN_NET_AMOUNT
|
||||
+ FieldBit.MAIN_NET_3D_AMOUNT
|
||||
+ FieldBit.MAIN_NET_5D_AMOUNT
|
||||
)
|
||||
df = self.get_board_members(
|
||||
board_symbol,
|
||||
sort_type=sort_type,
|
||||
sort_order=sort_order,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
agg_keys = ("amount", "main_net_amount", "main_net_3d_amount", "main_net_5d_amount")
|
||||
numeric_cols = [c for c in agg_keys if c in df.columns]
|
||||
sums = df[numeric_cols].sum() if numeric_cols else pd.Series(dtype=float)
|
||||
|
||||
close_col = "close" if "close" in df.columns else None
|
||||
pre_close_col = "pre_close" if "pre_close" in df.columns else None
|
||||
if close_col and pre_close_col:
|
||||
diff = df[close_col] - df[pre_close_col]
|
||||
up_count = int((diff > 0).sum())
|
||||
down_count = int((diff < 0).sum())
|
||||
else:
|
||||
up_count = down_count = 0
|
||||
|
||||
return {
|
||||
"member_count": len(df),
|
||||
"amount": float(sums.get("amount", 0.0)),
|
||||
"vol": int(df["vol"].sum()) if "vol" in df.columns else 0,
|
||||
"main_net_amount": float(sums.get("main_net_amount", 0.0)),
|
||||
"main_net_3d": float(sums.get("main_net_3d_amount", 0.0)),
|
||||
"main_net_5d": float(sums.get("main_net_5d_amount", 0.0)),
|
||||
"up_count": up_count,
|
||||
"down_count": down_count,
|
||||
"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
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 资金流向
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -1011,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))
|
||||
@@ -1027,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))
|
||||
@@ -1052,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(
|
||||
@@ -1153,6 +1292,146 @@ class AsyncMacClient:
|
||||
items = await self._execute(SymbolBelongBoardCmd(market, code))
|
||||
return _to_df(items)
|
||||
|
||||
async def get_board_summary(
|
||||
self,
|
||||
board_symbol: str,
|
||||
sort_type: SortType = SortType.CHANGE_PCT,
|
||||
sort_order: SortOrder = SortOrder.DESC,
|
||||
) -> dict[str, Any]:
|
||||
"""获取板块汇总:总成交金额、主力资金流向等(聚合成分股数据)。
|
||||
|
||||
基于 ``get_board_members`` 获取全部成分股报价,对成交额和资金流字段求和。
|
||||
|
||||
Args:
|
||||
board_symbol: 板块代码(如 "881001")。
|
||||
sort_type: 排序字段。
|
||||
sort_order: 排序方向。
|
||||
|
||||
Returns:
|
||||
包含以下键的字典::
|
||||
|
||||
member_count 成分股数量
|
||||
amount 板块总成交额(元)
|
||||
vol 板块总成交量(股)
|
||||
main_net_amount 板块主力净流入(元)
|
||||
main_net_3d 板块近3日主力净流入(元)
|
||||
main_net_5d 板块近5日主力净流入(元)
|
||||
up_count 上涨家数
|
||||
down_count 下跌家数
|
||||
members 成分股明细 DataFrame
|
||||
"""
|
||||
from ..codec.bitmap import FieldBit, PresetField
|
||||
|
||||
fields = (
|
||||
PresetField.BASIC
|
||||
+ FieldBit.AMOUNT
|
||||
+ FieldBit.MAIN_NET_AMOUNT
|
||||
+ FieldBit.MAIN_NET_3D_AMOUNT
|
||||
+ FieldBit.MAIN_NET_5D_AMOUNT
|
||||
)
|
||||
df = await self.get_board_members(
|
||||
board_symbol,
|
||||
sort_type=sort_type,
|
||||
sort_order=sort_order,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
agg_keys = ("amount", "main_net_amount", "main_net_3d_amount", "main_net_5d_amount")
|
||||
numeric_cols = [c for c in agg_keys if c in df.columns]
|
||||
sums = df[numeric_cols].sum() if numeric_cols else pd.Series(dtype=float)
|
||||
|
||||
close_col = "close" if "close" in df.columns else None
|
||||
pre_close_col = "pre_close" if "pre_close" in df.columns else None
|
||||
if close_col and pre_close_col:
|
||||
diff = df[close_col] - df[pre_close_col]
|
||||
up_count = int((diff > 0).sum())
|
||||
down_count = int((diff < 0).sum())
|
||||
else:
|
||||
up_count = down_count = 0
|
||||
|
||||
return {
|
||||
"member_count": len(df),
|
||||
"amount": float(sums.get("amount", 0.0)),
|
||||
"vol": int(df["vol"].sum()) if "vol" in df.columns else 0,
|
||||
"main_net_amount": float(sums.get("main_net_amount", 0.0)),
|
||||
"main_net_3d": float(sums.get("main_net_3d_amount", 0.0)),
|
||||
"main_net_5d": float(sums.get("main_net_5d_amount", 0.0)),
|
||||
"up_count": up_count,
|
||||
"down_count": down_count,
|
||||
"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
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 资金流向
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
Reference in New Issue
Block a user