mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 14:34:15 +08:00
release: v1.17.6 — 港股逐笔成交全量取数 + start 倒序语义文档
回应 issue #14 后用户反馈:默认 count=2000 取回的成交时间全集中在尾盘。 根因是通达信逐笔协议(A 股 0x122F 与港股 ex 0x23FC/0x2406 一致)的 start 为 倒序语义——start=0 指向最新一笔(收盘方向),并非 bug。02715 全天成交 13327 笔,count=2000 只取最近 2000 笔故集中在尾盘。 新增: - goods_transaction_all(MacExClient 同步+异步)—— 港股股票类市场自动按 1800/页 翻页取全天全部逐笔成交,安全上限 50 页(90000 条)。返回协议原生倒序,需正序 由调用方 df.iloc[::-1]。market 非港股时抛 ValueError。 - _fetch_all_hk_transactions_sync/async(_hk_transaction.py)底层实现。 变更: - goods_transaction docstring 补 start 倒序语义说明,引导需全天数据用 goods_transaction_all。 修复: - 1.17.5 的 test_hk_transaction.py 未过 CI ruff format --check(注释对齐、 MacTransaction 单行化、文末空行)。 867 单测全绿(+5 全量取数测试),ruff format/check / mypy strict 通过。
This commit is contained in:
@@ -2,6 +2,22 @@
|
||||
|
||||
本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
|
||||
|
||||
## [1.17.6] — 2026-07-04
|
||||
|
||||
**港股逐笔成交:补充 start 倒序语义文档 + 新增 goods_transaction_all 全量取数** —— 回应 issue #14 后用户反馈:默认 `count=2000` 取回的成交记录时间全集中在尾盘(如 02715 全天成交 13327 笔,count=2000 只取到最近 2000 笔)。根因是通达信逐笔协议(A 股 0x122F 与港股 ex 0x23FC/0x2406 一致)的 `start` 为**倒序**语义——start=0 指向最新一笔(收盘方向),并非 bug。本次:补 docstring 说明 start 语义;新增 `goods_transaction_all` 自动翻页取全天全部成交。**867 单测全绿**(+5),ruff format/check / mypy strict 通过。
|
||||
|
||||
### 新增
|
||||
|
||||
- **`goods_transaction_all`(全量取数)**(`src/easy_tdx/ex/mac_client.py` 同步 + 异步、`src/easy_tdx/ex/_hk_transaction.py` 新增 `_fetch_all_hk_transactions_sync/async`)—— 港股股票类市场专用,自动按 1800/页翻页直至末页(不足一页或空即停),返回当日全部逐笔成交(港股单日常 1~5 万笔)。安全上限 50 页(90000 条)防止异常数据导致无限翻页。返回顺序为协议原生倒序(最新在前);需正序展示由调用方自行 `df.iloc[::-1]`。market 非港股股票类时抛 `ValueError`。
|
||||
|
||||
### 变更
|
||||
|
||||
- **`goods_transaction` docstring 补 start 倒序语义**(`src/easy_tdx/ex/mac_client.py`)—— 明确说明 `start=0` 指向最新一笔(收盘方向),与 A 股 0x122F 语义一致;提示 `count=2000` 默认只取最近 2000 笔会集中在尾盘,需全天数据请用 `goods_transaction_all`。
|
||||
|
||||
### 修复
|
||||
|
||||
- **CI ruff format 失败**(`tests/unit/test_hk_transaction.py`)—— 1.17.5 引入的测试文件未过 `ruff format --check`(参数化注释前双空格、MacTransaction 单行化、文末空行)。本次顺手修复。
|
||||
|
||||
## [1.17.5] — 2026-07-04
|
||||
|
||||
**港股逐笔成交协议路由修复** —— 修复 issue #14:`MacExClient.goods_transaction` 对港股市场(HK 主板 / 创业板 / 指数 / 基金 / 港股通 / 暗盘)返回空。根因是对所有扩展市场统一复用了 A 股 MAC 协议的 `SymbolTransactionCmd`(0x122F),而 0x122F 的数据源未接入港股,服务器对港股 market 一律返回 39 字节空响应(count=0)。改为对港股股票类市场路由到 ex 扩展行情协议(当日 0x23FC / 历史 0x2406),并把整数价格换算为港元浮点。**860 单测全绿**(+22),ruff / mypy strict 通过。
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "easy-tdx"
|
||||
version = "1.17.5"
|
||||
version = "1.17.6"
|
||||
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -156,3 +156,60 @@ async def _fetch_hk_transactions_async(
|
||||
if len(batch) < page_size:
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
# 全量翻页的安全上限:50 页 × 1800 = 90000 条,覆盖港股单日成交峰值绰绰有余。
|
||||
# 超过即停,防止异常数据(如服务器循环返回)导致无限翻页。
|
||||
_HK_TRANSACTION_MAX_PAGES = 50
|
||||
|
||||
|
||||
def _fetch_all_hk_transactions_sync(
|
||||
execute_fn: SyncExecute[list[ExTransactionRecord]],
|
||||
market: int,
|
||||
code: str,
|
||||
query_date: date | None,
|
||||
start: int = 0,
|
||||
) -> list[MacTransaction]:
|
||||
"""同步获取港股某日**全部**逐笔成交(自动翻页直至末页)。
|
||||
|
||||
0x23FC/0x2406 响应不含 total 字段,只能按页翻到不足一页或空为止。
|
||||
返回顺序与协议一致(倒序:start=0 为最新/收盘方向)。
|
||||
"""
|
||||
ymd = _to_ymd(query_date) if query_date is not None else None
|
||||
|
||||
results: list[MacTransaction] = []
|
||||
offset = start
|
||||
for _ in range(_HK_TRANSACTION_MAX_PAGES):
|
||||
cmd = _build_cmd(market, code, ymd, offset, _HK_TRANSACTION_PAGE_SIZE)
|
||||
batch = execute_fn(cmd)
|
||||
if not batch:
|
||||
break
|
||||
results.extend(_map_record(r) for r in batch)
|
||||
offset += len(batch)
|
||||
if len(batch) < _HK_TRANSACTION_PAGE_SIZE:
|
||||
break # 末页
|
||||
return results
|
||||
|
||||
|
||||
async def _fetch_all_hk_transactions_async(
|
||||
execute_fn: AsyncExecute[list[ExTransactionRecord]],
|
||||
market: int,
|
||||
code: str,
|
||||
query_date: date | None,
|
||||
start: int = 0,
|
||||
) -> list[MacTransaction]:
|
||||
"""异步获取港股某日**全部**逐笔成交。语义同同步版 :func:`_fetch_all_hk_transactions_sync`。"""
|
||||
ymd = _to_ymd(query_date) if query_date is not None else None
|
||||
|
||||
results: list[MacTransaction] = []
|
||||
offset = start
|
||||
for _ in range(_HK_TRANSACTION_MAX_PAGES):
|
||||
cmd = _build_cmd(market, code, ymd, offset, _HK_TRANSACTION_PAGE_SIZE)
|
||||
batch = await execute_fn(cmd)
|
||||
if not batch:
|
||||
break
|
||||
results.extend(_map_record(r) for r in batch)
|
||||
offset += len(batch)
|
||||
if len(batch) < _HK_TRANSACTION_PAGE_SIZE:
|
||||
break # 末页
|
||||
return results
|
||||
|
||||
@@ -27,6 +27,8 @@ from ..mac.commands.symbol_transaction import SymbolTransactionCmd
|
||||
from ..mac.enums import Adjust, Period, SortOrder, SortType
|
||||
from ..mac.models import MacQuoteField
|
||||
from ._hk_transaction import (
|
||||
_fetch_all_hk_transactions_async,
|
||||
_fetch_all_hk_transactions_sync,
|
||||
_fetch_hk_transactions_async,
|
||||
_fetch_hk_transactions_sync,
|
||||
is_hk_stock_market,
|
||||
@@ -426,9 +428,12 @@ class MacExClient:
|
||||
query_date : date | None
|
||||
查询日期,None 表示今天。
|
||||
start : int
|
||||
起始偏移。
|
||||
起始偏移。**注意:通达信逐笔协议为倒序**——``start=0`` 指向最新一笔
|
||||
(收盘方向),``start`` 越大越早。A 股 0x122F 与港股 ex 协议语义一致。
|
||||
count : int
|
||||
返回条数。
|
||||
返回条数。港股单日成交常达数万笔(如 02715 约 1.3 万笔/日),默认
|
||||
``count=2000`` 只取最近 2000 笔,会集中在尾盘时段。若需全天全部成交,
|
||||
请改用 :meth:`goods_transaction_all`。
|
||||
|
||||
Note
|
||||
----
|
||||
@@ -454,6 +459,42 @@ class MacExClient:
|
||||
result = self._execute(cmd)
|
||||
return _to_df(result)
|
||||
|
||||
def goods_transaction_all(
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
query_date: date | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""获取港股某日**全部**逐笔成交(仅港股股票类市场,自动翻页取全天)。
|
||||
|
||||
与 :meth:`goods_transaction` 的区别:不受 ``count`` 上限约束,自动翻页直至
|
||||
末页,返回当日所有逐笔成交(港股单日常 1~5 万笔)。返回顺序仍为协议原生
|
||||
倒序(最新在前);如需正序展示,调用方自行 ``df.iloc[::-1]`` 反转。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
market : int
|
||||
ExMarket 枚举值(须为港股股票类市场,见
|
||||
:data:`easy_tdx.ex._hk_transaction.HK_STOCK_MARKETS`)。
|
||||
code : str
|
||||
证券代码。
|
||||
query_date : date | None
|
||||
查询日期,None 表示今天。
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
``market`` 不属于港股股票类市场时抛出(本方法专为港股设计;其他扩展
|
||||
市场请用 :meth:`goods_transaction`)。
|
||||
"""
|
||||
if not is_hk_stock_market(market):
|
||||
raise ValueError(
|
||||
f"goods_transaction_all 仅支持港股股票类市场(HK_STOCK_MARKETS),"
|
||||
f"收到 market={market};其他市场请用 goods_transaction。"
|
||||
)
|
||||
result = _fetch_all_hk_transactions_sync(self._execute, market, code, query_date)
|
||||
return _to_df(result)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 异步客户端
|
||||
@@ -739,7 +780,10 @@ class AsyncMacExClient(AsyncHeartbeatMixin):
|
||||
start: int = 0,
|
||||
count: int = 2000,
|
||||
) -> pd.DataFrame:
|
||||
"""获取逐笔成交数据(异步)。路由说明见同步版 :meth:`goods_transaction`。"""
|
||||
"""获取逐笔成交数据(异步)。
|
||||
|
||||
路由与 ``start`` 倒序语义见同步版 :meth:`goods_transaction`。
|
||||
"""
|
||||
if is_hk_stock_market(market):
|
||||
result = await _fetch_hk_transactions_async(
|
||||
self._execute, market, code, query_date, start, count
|
||||
@@ -754,3 +798,18 @@ class AsyncMacExClient(AsyncHeartbeatMixin):
|
||||
)
|
||||
result = await self._execute(cmd)
|
||||
return _to_df(result)
|
||||
|
||||
async def goods_transaction_all(
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
query_date: date | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""获取港股某日全部逐笔成交(异步)。语义见同步版 :meth:`goods_transaction_all`。"""
|
||||
if not is_hk_stock_market(market):
|
||||
raise ValueError(
|
||||
f"goods_transaction_all 仅支持港股股票类市场(HK_STOCK_MARKETS),"
|
||||
f"收到 market={market};其他市场请用 goods_transaction。"
|
||||
)
|
||||
result = await _fetch_all_hk_transactions_async(self._execute, market, code, query_date)
|
||||
return _to_df(result)
|
||||
|
||||
@@ -333,3 +333,101 @@ async def test_async_goods_transaction_non_hk_keeps_0x122f():
|
||||
assert isinstance(captured[0], SymbolTransactionCmd)
|
||||
assert len(df) == 1
|
||||
assert df["price"].iloc[0] == pytest.approx(3850.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. goods_transaction_all 全量取数(mock _execute,离线)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_goods_transaction_all_paginates_until_short_page():
|
||||
"""全量取数:翻页直到某页返回不足 page_size(末页)即停。"""
|
||||
from easy_tdx.ex.mac_client import MacExClient
|
||||
|
||||
page_calls: list[int] = [] # 记录每页的 start
|
||||
|
||||
def fake_execute(cmd):
|
||||
page_calls.append(cmd.start)
|
||||
# 前 3 页满页(1800),第 4 页返回 500(末页)
|
||||
if cmd.start < 1800 * 3:
|
||||
return _build_fake_records(cmd.count)
|
||||
return _build_fake_records(500)
|
||||
|
||||
client = object.__new__(MacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = client.goods_transaction_all(31, "00700", date(2026, 7, 3))
|
||||
|
||||
assert len(page_calls) == 4 # 3 满页 + 1 末页
|
||||
assert page_calls == [0, 1800, 3600, 5400]
|
||||
assert len(df) == 1800 * 3 + 500
|
||||
|
||||
|
||||
def test_goods_transaction_all_stops_on_empty():
|
||||
"""全量取数:第一页空(休市日/无数据)应立即返回空。"""
|
||||
from easy_tdx.ex.mac_client import MacExClient
|
||||
|
||||
call_count = 0
|
||||
|
||||
def fake_execute(cmd):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return []
|
||||
|
||||
client = object.__new__(MacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = client.goods_transaction_all(31, "00700", date(2026, 7, 1))
|
||||
|
||||
assert call_count == 1
|
||||
assert len(df) == 0
|
||||
|
||||
|
||||
def test_goods_transaction_all_rejects_non_hk_market():
|
||||
"""全量取数仅限港股股票类市场;其他市场应报 ValueError。"""
|
||||
from easy_tdx.ex.mac_client import MacExClient
|
||||
|
||||
client = object.__new__(MacExClient)
|
||||
client._execute = lambda cmd: [] # type: ignore[method-assign]
|
||||
|
||||
with pytest.raises(ValueError, match="港股股票类市场"):
|
||||
client.goods_transaction_all(47, "IFL0") # CFFEX 期货
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_goods_transaction_all_paginates():
|
||||
"""异步全量取数也按页翻到末页停止。"""
|
||||
from easy_tdx.ex.mac_client import AsyncMacExClient
|
||||
|
||||
page_calls: list[int] = []
|
||||
|
||||
async def fake_execute(cmd):
|
||||
page_calls.append(cmd.start)
|
||||
# 前 1 页满页,第 2 页返回 100(末页)
|
||||
if cmd.start == 0:
|
||||
return _build_fake_records(cmd.count)
|
||||
return _build_fake_records(100)
|
||||
|
||||
client = object.__new__(AsyncMacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = await client.goods_transaction_all(31, "00700", date(2026, 7, 3))
|
||||
|
||||
assert page_calls == [0, 1800]
|
||||
assert len(df) == 1800 + 100
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_goods_transaction_all_rejects_non_hk():
|
||||
"""异步全量取数:非港股市场报 ValueError。"""
|
||||
from easy_tdx.ex.mac_client import AsyncMacExClient
|
||||
|
||||
client = object.__new__(AsyncMacExClient)
|
||||
|
||||
async def fake_execute(cmd):
|
||||
return []
|
||||
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
with pytest.raises(ValueError, match="港股股票类市场"):
|
||||
await client.goods_transaction_all(74, "AAPL") # 美股
|
||||
|
||||
Reference in New Issue
Block a user