release: v1.17.5 — 港股逐笔成交协议路由修复 (issue #14)

MacExClient.goods_transaction 此前对所有扩展市场统一复用 A 股 MAC 协议的
SymbolTransactionCmd(0x122F),但 0x122F 数据源未接入港股,服务器对港股
market 一律返回 39 字节空响应(count=0)。

港股股票类市场(HK_STOCK_MARKETS = {27,31,48,49,71,98})改走 ex 扩展行情
协议:query_date=None → GetExTransactionDataCmd(0x23FC 当日),指定日期 →
GetExHistoryTransactionDataCmd(0x2406 历史)。返回的 ExTransactionRecord
(price 为整数、单位 0.001 HKD)映射为与 A 股 MacTransaction 一致的 schema,
价格 ÷1000 换算为港元浮点,与港股分时图 float 价格对齐。count > 1800 时按
1800/页自动分页。其余扩展市场(美股 / 期货等)保持 MAC 0x122F 路径不变。

新增 src/easy_tdx/ex/_hk_transaction.py(市场判定 + 字段映射 + 自动分页,
同步/异步共用);回归测试 24 例(tests/unit/test_hk_transaction.py)+ 真实
fixture(港股 00700 在 2026-07-03 的 0x2406 响应)。862 单测全绿,ruff /
mypy strict 通过。
This commit is contained in:
Justin Gu
2026-07-04 01:29:17 +08:00
parent 648c9ccfaf
commit 40ae2eba53
7 changed files with 561 additions and 1 deletions
+158
View File
@@ -0,0 +1,158 @@
"""港股逐笔成交的协议路由辅助。
背景(issue #14):``MacExClient.goods_transaction`` 原先对所有扩展市场统一复用
A 股 MAC 协议的 ``SymbolTransactionCmd``0x122F)。但 0x122F 的数据源只覆盖沪深京
A 股 + 部分扩展市场(美股 / 中金所期货恰好接入),**唯独港股未接入**,服务器对港股
market 一律返回 39 字节空响应(count=0)。
港股逐笔成交的正确协议是 ex 扩展行情层:
- 当日(``query_date is None``)→ ``GetExTransactionDataCmd``0x23FC
- 历史(指定 ``query_date``)→ ``GetExHistoryTransactionDataCmd``0x2406
返回的 ``ExTransactionRecord`` 字段(hour/minute/second/price:int/volume/zengcang/
nature)需映射为与 A 股 ``MacTransaction`` 兼容的 schema,并把整数价格换算为港元
浮点(单位 0.001 HKD,与港股分时图 float 价格一致)。
同步 / 异步共用本模块:``execute_fn`` 由调用方注入——同步版传 ``self._execute``
异步版传 ``self._execute``(协程回调)。
"""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from datetime import date
from typing import TypeVar
from ..commands.base import BaseCommand
from ..mac.models import MacTransaction
from .commands.get_transaction import GetExHistoryTransactionDataCmd, GetExTransactionDataCmd
from .models import ExTransactionRecord
# 港股股票类市场(走 ex 协议 0x23FC / 0x2406)。
# 不含衍生品(HK_FINANCIAL_FUTURES=23 / HK_FINANCIAL_OPTIONS=24 / HK_STOCK_FUTURES=25 /
# HK_STOCK_OPTIONS=26):期货 / 期权逐笔语义不同,且 0x122F 对 CFFEX 期货恰好可用,
# 本次不改变其行为以避免回归。
HK_STOCK_MARKETS: frozenset[int] = frozenset(
{
27, # HK_INDEX 香港指数
31, # HK_MAIN_BOARD 香港主板
48, # HK_GEM 香港创业板
49, # HK_FUND 香港基金
71, # HK_STOCK_GGT 港股-港股通
98, # HK_DARK_POOL 港股暗盘
}
)
# ex 协议单页最大返回条数(与 GetExTransactionDataCmd 默认 count 一致)。
_HK_TRANSACTION_PAGE_SIZE = 1800
# 港股价格整数单位:1 港元 = 1000,即返回的 price_int / 1000 = 港元。
# 与港股分时图(0x248b)返回的 float 价格对齐验证过(如 431400 → 431.4 HKD)。
_HK_PRICE_DIVISOR = 1000.0
_T = TypeVar("_T")
# 同步执行回调:传入 BaseCommand,返回其 parse_response 结果
SyncExecute = Callable[[BaseCommand[_T]], _T]
# 异步执行回调:传入 BaseCommand,返回可等待的 parse_response 结果
AsyncExecute = Callable[[BaseCommand[_T]], Awaitable[_T]]
def is_hk_stock_market(market: int) -> bool:
"""判断给定市场代码是否属于港股股票类(需走 ex 协议取逐笔成交)。"""
return market in HK_STOCK_MARKETS
def _to_ymd(query_date: date) -> int:
"""date → YYYYMMDD intex 历史命令的日期参数格式)。"""
return query_date.year * 10000 + query_date.month * 100 + query_date.day
def _build_cmd(
market: int,
code: str,
ymd: int | None,
offset: int,
page_size: int,
) -> BaseCommand[list[ExTransactionRecord]]:
"""根据是否有日期构建对应的 ex 协议命令。"""
if ymd is None:
return GetExTransactionDataCmd(market, code, offset, page_size)
return GetExHistoryTransactionDataCmd(market, code, ymd, offset, page_size)
def _map_record(rec: ExTransactionRecord) -> MacTransaction:
"""把 ex 协议的 ExTransactionRecord 映射为与 A 股一致的 MacTransaction。
- price: 整数 → 港元浮点(÷1000)
- vol: volume 原样
- trade_count: ex 协议无此字段,置 0
- bs_flag: 取 nature(买卖方向标志,语义近似;0=买/1=卖/2=中性 等)
- time: 由 hour/minute/second 组合
"""
from datetime import time as time_cls
return MacTransaction(
time=time_cls(rec.hour, rec.minute, rec.second),
price=rec.price / _HK_PRICE_DIVISOR,
vol=rec.volume,
trade_count=0,
bs_flag=rec.nature,
)
def _fetch_hk_transactions_sync(
execute_fn: SyncExecute[list[ExTransactionRecord]],
market: int,
code: str,
query_date: date | None,
start: int,
count: int,
) -> list[MacTransaction]:
"""同步获取港股逐笔成交(自动分页)。"""
ymd = _to_ymd(query_date) if query_date is not None else None
results: list[MacTransaction] = []
fetched = 0
offset = start
while fetched < count:
page_size = min(count - fetched, _HK_TRANSACTION_PAGE_SIZE)
cmd = _build_cmd(market, code, ymd, offset, page_size)
batch = execute_fn(cmd)
if not batch:
break
results.extend(_map_record(r) for r in batch)
fetched += len(batch)
offset += len(batch)
if len(batch) < page_size:
break
return results
async def _fetch_hk_transactions_async(
execute_fn: AsyncExecute[list[ExTransactionRecord]],
market: int,
code: str,
query_date: date | None,
start: int,
count: int,
) -> list[MacTransaction]:
"""异步获取港股逐笔成交(自动分页)。"""
ymd = _to_ymd(query_date) if query_date is not None else None
results: list[MacTransaction] = []
fetched = 0
offset = start
while fetched < count:
page_size = min(count - fetched, _HK_TRANSACTION_PAGE_SIZE)
cmd = _build_cmd(market, code, ymd, offset, page_size)
batch = await execute_fn(cmd)
if not batch:
break
results.extend(_map_record(r) for r in batch)
fetched += len(batch)
offset += len(batch)
if len(batch) < page_size:
break
return results
+25
View File
@@ -26,6 +26,11 @@ from ..mac.commands.symbol_tick_chart import SymbolTickChartCmd
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_hk_transactions_async,
_fetch_hk_transactions_sync,
is_hk_stock_market,
)
from .commands.get_instrument_count import GetExInstrumentCountCmd
from .commands.get_instrument_info import GetExInstrumentInfoCmd
from .commands.login import MacExLoginCmd
@@ -424,7 +429,21 @@ class MacExClient:
起始偏移。
count : int
返回条数。
Note
----
港股股票类市场(HK_MAIN_BOARD/HK_GEM/HK_INDEX/HK_FUND/HK_STOCK_GGT/HK_DARK_POOL
见 :data:`easy_tdx.ex._hk_transaction.HK_STOCK_MARKETS`)走 ex 扩展行情协议
(当日 0x23FC / 历史 0x2406),返回价格单位为港元(浮点)。其余扩展市场
(美股 / 期货等)走 MAC 协议 0x122F。原因:0x122F 的数据源未接入港股,
对港股请求会返回空(issue #14)。不确定市场归属时,可先用
:meth:`goods_kline` 探测哪个 market 能取到 K 线。
"""
if is_hk_stock_market(market):
result = _fetch_hk_transactions_sync(
self._execute, market, code, query_date, start, count
)
return _to_df(result)
cmd = SymbolTransactionCmd(
market=market,
code=code,
@@ -720,6 +739,12 @@ class AsyncMacExClient(AsyncHeartbeatMixin):
start: int = 0,
count: int = 2000,
) -> pd.DataFrame:
"""获取逐笔成交数据(异步)。路由说明见同步版 :meth:`goods_transaction`。"""
if is_hk_stock_market(market):
result = await _fetch_hk_transactions_async(
self._execute, market, code, query_date, start, count
)
return _to_df(result)
cmd = SymbolTransactionCmd(
market=market,
code=code,