Files
easy-tdx/src/easy_tdx/ex/mac_client.py
T
GitHub d0d65d64b8 feat: v1.20.0 服务器失败自动 ping 切换(无需手动 easy-tdx ping)
服务器连不上或返回空数据时,自动测速切到延迟最低的可用服务器再试,
Python API / CLI / Web API 三入口全部自动生效。

核心改动:
- _reconnect.py 新增 select_best_host_sync/async(连接失败 failover)
  和 find_working_host_sync/async(空数据逐台实测)
- 8 个 client 的 _execute 注入跨主机故障转移(复用 auto_reconnect 开关)
- get_market_stat 空数据时按延迟顺序逐台实测找返回数据的服务器
- 新增 _reconnect/_areconnect helper 收敛重建连接副本
- MacClient failover 用 save_best_mac_host(延续 v1.19.4 不污染 best_host)
- 顺手修复 test_commands_offline 未使用 import(main CI failure 根因)

测试:925 passed(新增 18 个 failover 测试),ruff/mypy 零新增错误。
2026-07-08 18:07:37 +08:00

864 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""MAC 协议扩展市场高层 APIMacExClient(同步)和 AsyncMacExClientasyncio)。
期货/港股/美股等扩展市场通过 MAC 协议命令(0x122B/0x122E/0x122D/0x122F/0x2562
获取数据,使用 ExTdxConnection(端口 7727,单包握手)。
"""
import asyncio
import logging
import time
from collections.abc import Awaitable
from datetime import date
from types import TracebackType
from typing import Any, TypeVar
import pandas as pd
from .._df import _to_df
from .._reconnect import (
_RETRY_DELAYS,
AsyncHeartbeatMixin,
select_best_host_async,
select_best_host_sync,
)
from ..commands.base import BaseCommand
from ..config import get_best_mac_ex_host, get_mac_ex_hosts, save_best_mac_ex_host
from ..exceptions import TdxConnectionError
from ..mac.commands.chart_sampling import ChartSamplingCmd
from ..mac.commands.symbol_bar import SymbolBarCmd
from ..mac.commands.symbol_quotes import SymbolQuotesCmd
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_all_hk_transactions_async,
_fetch_all_hk_transactions_sync,
_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
from .transport.async_ import AsyncExTdxConnection
from .transport.sync import ExTdxConnection, ping_ex_all
_DEFAULT_PORT = 7727
_T = TypeVar("_T")
logger = logging.getLogger(__name__)
def _quotes_to_df(result: list[MacQuoteField]) -> pd.DataFrame:
"""将 MacQuoteField 列表展开为 DataFrame。"""
rows: list[dict[str, Any]] = []
for item in result:
row: dict[str, Any] = {"market": item.market, "code": item.code, "name": item.name}
row.update(item.fields)
rows.append(row)
return pd.DataFrame(rows) if rows else pd.DataFrame()
# ============================================================
# 同步客户端
# ============================================================
class MacExClient:
"""同步 MAC 协议扩展市场客户端(期货/港股/美股,端口 7727)。
使用示例::
with MacExClient() as c:
df = c.goods_kline(ExMarket.CFFEX_FUTURES, "IFL0", Period.DAILY)
df = c.goods_quotes([(ExMarket.HK_MAIN_BOARD, "00700")])
"""
def __init__(
self,
host: str | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
auto_reconnect: bool = True,
) -> None:
self._host = host if host is not None else get_best_mac_ex_host()
self._port = port
self._timeout = timeout
self._auto_reconnect = auto_reconnect
self._conn = ExTdxConnection(self._host, port, timeout, mac_ex_mode=True)
@classmethod
def from_best_host(
cls,
hosts: list[str] | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
ping_timeout: float = 5.0,
auto_reconnect: bool = True,
) -> "MacExClient":
"""测量所有 MAC 扩展行情服务器延迟,选最低延迟建立连接。"""
candidates = hosts or get_mac_ex_hosts()
ranked = ping_ex_all(candidates, port, ping_timeout)
best = ranked[0][0] if ranked else candidates[0]
save_best_mac_ex_host(best)
return cls(best, port, timeout, auto_reconnect)
@staticmethod
def ping_all(
hosts: list[str] | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 5.0,
) -> list[tuple[str, float]]:
return ping_ex_all(hosts or get_mac_ex_hosts(), port, timeout)
# ------------------------------------------------------------------ #
# 连接管理
# ------------------------------------------------------------------ #
def connect(self) -> None:
self._conn.connect()
self._login()
def close(self) -> None:
self._conn.close()
def disconnect(self) -> None:
self.close()
def ensure_connected(self) -> None:
"""验证连接存活,断线则自动重建。"""
try:
self._execute(GetExInstrumentCountCmd())
except TdxConnectionError:
self._conn.close()
self._conn = ExTdxConnection(self._host, self._port, self._timeout, mac_ex_mode=True)
self._conn.connect()
self._login()
def __enter__(self) -> "MacExClient":
self.connect()
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.close()
def _login(self) -> None:
"""执行 MAC EX 登录命令。"""
self._conn.execute(MacExLoginCmd())
def _execute(self, cmd: "BaseCommand[_T]") -> _T:
"""执行命令;断线时指数退避重试,同主机耗尽则跨主机故障转移。
每次重连后必须重新 ``_login()``(MAC 协议扩展行情特有)。登录握手期的
``TdxConnectionError`` 与业务请求一样计入退避重试;``TdxCommandError``
(登录被拒等确定性失败)不重试,直接抛出。跨主机故障转移阶段同样遵循
``connect + login`` 纳入重试的语义。
"""
try:
return self._conn.execute(cmd)
except TdxConnectionError:
if not self._auto_reconnect:
raise
last_exc: TdxConnectionError | None = None
for delay in _RETRY_DELAYS:
time.sleep(delay)
self._conn.close()
self._conn = ExTdxConnection(
self._host, self._port, self._timeout, mac_ex_mode=True
)
# connect + login 纳入重试:登录握手期连接再次断开属可重试语义。
try:
self._conn.connect()
self._login()
return self._conn.execute(cmd)
except TdxConnectionError as e:
last_exc = e
# 第二阶段:跨主机故障转移——测速切到另一台 MAC 扩展行情服务器
new_host = select_best_host_sync(
get_mac_ex_hosts(),
ping_ex_all,
save_best_mac_ex_host,
self._port,
5.0,
self._host,
)
if new_host is not None:
self._host = new_host
self._conn.close()
self._conn = ExTdxConnection(
self._host, self._port, self._timeout, mac_ex_mode=True
)
try:
self._conn.connect()
self._login()
return self._conn.execute(cmd)
except TdxConnectionError as e:
last_exc = e
raise last_exc # type: ignore[misc]
# ------------------------------------------------------------------ #
# 商品列表
# ------------------------------------------------------------------ #
def goods_count(self, market: int | None = None) -> int:
"""获取商品总数。market=None 时返回全市场总数,否则返回指定市场的数量。"""
if market is None:
return self._execute(GetExInstrumentCountCmd())
# 需要二分查找定位市场边界来计数
offset = self._find_market_offset(market)
if offset < 0:
return 0
total = self._execute(GetExInstrumentCountCmd())
# 从 offset 开始扫描计数
n = 0
page = 1000
pos = offset
while pos < total:
batch = self._execute(GetExInstrumentInfoCmd(start=pos, count=page))
if not batch:
break
for item in batch:
if item.market == market:
n += 1
elif item.market > market:
return n
pos += page
return n
def goods_list(self, market: int, start: int = 0, count: int = 600) -> pd.DataFrame:
"""获取扩展市场商品列表(期货合约/港股/美股等)。
通过 EX 协议的 GetInstrumentInfo 命令获取,按 market 过滤。
Parameters
----------
market : int
ExMarket 枚举值,如 ExMarket.HK_MAIN_BOARD。
start : int
市场内起始偏移。
count : int
请求数量。
"""
offset = self._find_market_offset(market)
if offset < 0:
return pd.DataFrame()
total = self._execute(GetExInstrumentCountCmd())
page_size = 1000
collected: list[Any] = []
skipped = 0
pos = offset
while pos < total and len(collected) < count:
batch = self._execute(GetExInstrumentInfoCmd(start=pos, count=page_size))
if not batch:
break
for item in batch:
if item.market == market:
if skipped < start:
skipped += 1
else:
collected.append(item)
if len(collected) >= count:
break
elif item.market > market:
break
else:
pos += page_size
continue
break
return _to_df(collected)
def _find_market_offset(self, market: int) -> int:
"""二分查找定位指定市场在全局商品列表中的起始偏移。"""
total = self._execute(GetExInstrumentCountCmd())
if total == 0:
return -1
lo, hi = 0, total
while lo < hi:
mid = (lo + hi) // 2
items = self._execute(GetExInstrumentInfoCmd(start=mid, count=1))
if not items:
hi = mid
continue
m = items[0].market
if m < market:
lo = mid + 1
else:
hi = mid
return lo
# ------------------------------------------------------------------ #
# 行情
# ------------------------------------------------------------------ #
def goods_quotes(
self,
stocks: list[tuple[int, str]],
fields: Any = None,
) -> pd.DataFrame:
"""批量获取扩展市场自定义字段报价。
Parameters
----------
stocks : list[tuple[int, str]]
[(ExMarketcode, code), ...] 列表,最多 80 只。
fields : Fields | None
字段选择,默认 PresetField.COMMON。
"""
cmd = SymbolQuotesCmd(stocks, fields)
result: list[MacQuoteField] = self._execute(cmd)
return _quotes_to_df(result)
def goods_quotes_list(
self,
market: int,
start: int = 0,
count: int = 100,
sort_type: SortType = SortType.CODE,
sort_order: SortOrder = SortOrder.NONE,
) -> pd.DataFrame:
"""获取扩展市场排序报价列表(通过 GoodsList + Quotes 组合)。
先获取商品列表,再批量查询报价。
Parameters
----------
market : int
ExMarket 枚举值。
start : int
起始偏移。
count : int
返回条数(最大 80,受报价批量限制)。
sort_type : SortType
排序字段(暂未实现排序,预留接口)。
sort_order : SortOrder
排序方向(暂未实现排序,预留接口)。
"""
page_size = min(count, 80)
items_df = self.goods_list(market, start=start, count=page_size)
if items_df.empty:
return pd.DataFrame()
stocks: list[tuple[int, str]] = []
for _, row in items_df.iterrows():
stocks.append((market, row["code"]))
cmd = SymbolQuotesCmd(stocks)
result: list[MacQuoteField] = self._execute(cmd)
return _quotes_to_df(result)
def goods_kline(
self,
market: int,
code: str,
period: Period = Period.DAILY,
start: int = 0,
count: int = 800,
adjust: Adjust = Adjust.NONE,
) -> pd.DataFrame:
"""获取扩展市场 K 线数据(支持复权)。
Parameters
----------
market : int
ExMarket 枚举值。
code : str
证券代码。
period : Period
K 线周期。
start : int
起始偏移(0=最新)。
count : int
返回条数。
adjust : Adjust
复权方式(NONE/QFQ/HFQ)。
"""
cmd = SymbolBarCmd(
market=market,
code=code,
period=period,
start=start,
count=count,
fq=adjust,
)
result = self._execute(cmd)
return _to_df(result)
# ------------------------------------------------------------------ #
# 分时
# ------------------------------------------------------------------ #
def goods_tick_chart(
self,
market: int,
code: str,
query_date: date | None = None,
) -> pd.DataFrame:
"""获取单日分时图。
Parameters
----------
market : int
ExMarket 枚举值。
code : str
证券代码。
query_date : date | None
查询日期,None 表示今天。
"""
cmd = SymbolTickChartCmd(market=market, code=code, query_date=query_date)
result = self._execute(cmd)
return _to_df(result)
def goods_chart_sampling(
self,
market: int,
code: str,
) -> pd.DataFrame:
"""获取分时缩略采样价格点(约 240 个点)。
Parameters
----------
market : int
ExMarket 枚举值。
code : str
证券代码。
"""
cmd = ChartSamplingCmd(market=market, code=code)
prices: list[float] = self._execute(cmd)
if not prices:
return pd.DataFrame()
return pd.DataFrame({"price": prices})
# ------------------------------------------------------------------ #
# 成交
# ------------------------------------------------------------------ #
def goods_transaction(
self,
market: int,
code: str,
query_date: date | None = None,
start: int = 0,
count: int = 2000,
) -> pd.DataFrame:
"""获取逐笔成交数据。
Parameters
----------
market : int
ExMarket 枚举值。
code : str
证券代码。
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
----
港股股票类市场(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,
query_date=query_date,
start=start,
count=count,
)
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)
# ============================================================
# 异步客户端
# ============================================================
class AsyncMacExClient(AsyncHeartbeatMixin):
"""异步 MAC 协议扩展市场客户端(asyncio,端口 7727)。
使用示例::
async with AsyncMacExClient() as c:
df = await c.goods_kline(ExMarket.CFFEX_FUTURES, "IFL0", Period.DAILY)
"""
def __init__(
self,
host: str | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
auto_reconnect: bool = True,
heartbeat_interval: float = 60.0,
) -> None:
self._host = host if host is not None else get_best_mac_ex_host()
self._port = port
self._timeout = timeout
self._auto_reconnect = auto_reconnect
self._heartbeat_interval = heartbeat_interval
self._conn = AsyncExTdxConnection(self._host, port, timeout, mac_ex_mode=True)
self._execute_lock = asyncio.Lock()
self._heartbeat_task: asyncio.Task[None] | None = None
@classmethod
def from_best_host(
cls,
hosts: list[str] | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 15.0,
ping_timeout: float = 5.0,
auto_reconnect: bool = True,
heartbeat_interval: float = 60.0,
) -> "AsyncMacExClient":
candidates = hosts or get_mac_ex_hosts()
ranked = ping_ex_all(candidates, port, ping_timeout)
best = ranked[0][0] if ranked else candidates[0]
save_best_mac_ex_host(best)
return cls(best, port, timeout, auto_reconnect, heartbeat_interval)
@staticmethod
def ping_all(
hosts: list[str] | None = None,
port: int = _DEFAULT_PORT,
timeout: float = 5.0,
) -> list[tuple[str, float]]:
return ping_ex_all(hosts or get_mac_ex_hosts(), port, timeout)
# ------------------------------------------------------------------ #
# 连接管理
# ------------------------------------------------------------------ #
async def connect(self) -> None:
await self._conn.connect()
await self._login()
self._start_heartbeat()
async def close(self) -> None:
await self._stop_heartbeat()
await self._conn.close()
async def __aenter__(self) -> "AsyncMacExClient":
await self.connect()
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
await self.close()
def _heartbeat_cmd(self) -> Awaitable[object]:
"""心跳使用的轻量请求(get_instrument_count,复用 _execute 重连)。"""
return self._execute(GetExInstrumentCountCmd())
async def _login(self) -> None:
"""执行 MAC EX 登录命令。"""
await self._conn.execute(MacExLoginCmd())
async def _execute(self, cmd: "BaseCommand[_T]") -> _T:
"""执行命令;断线时指数退避重试,同主机耗尽则跨主机故障转移。
每次重连后必须重新 ``_login()``(MAC 协议扩展行情特有)。登录握手期的
``TdxConnectionError`` 与业务请求一样计入退避重试;``TdxCommandError``
(登录被拒等确定性失败)不重试,直接抛出。
"""
async with self._execute_lock:
try:
return await self._conn.execute(cmd)
except TdxConnectionError:
if not self._auto_reconnect:
raise
last_exc: TdxConnectionError | None = None
for delay in _RETRY_DELAYS:
await asyncio.sleep(delay)
await self._conn.close()
self._conn = AsyncExTdxConnection(
self._host, self._port, self._timeout, mac_ex_mode=True
)
# connect + login 纳入重试:登录握手期连接再次断开属可重试语义。
try:
await self._conn.connect()
await self._login()
return await self._conn.execute(cmd)
except TdxConnectionError as e:
last_exc = e
# 第二阶段:跨主机故障转移
new_host = await select_best_host_async(
get_mac_ex_hosts(),
ping_ex_all,
save_best_mac_ex_host,
self._port,
5.0,
self._host,
)
if new_host is not None:
self._host = new_host
await self._conn.close()
self._conn = AsyncExTdxConnection(
self._host, self._port, self._timeout, mac_ex_mode=True
)
try:
await self._conn.connect()
await self._login()
return await self._conn.execute(cmd)
except TdxConnectionError as e:
last_exc = e
raise last_exc # type: ignore[misc]
# ------------------------------------------------------------------ #
# 商品列表
# ------------------------------------------------------------------ #
async def goods_count(self, market: int | None = None) -> int:
"""获取商品总数。market=None 时返回全市场总数,否则返回指定市场的数量。"""
if market is None:
return await self._execute(GetExInstrumentCountCmd())
offset = await self._find_market_offset(market)
if offset < 0:
return 0
total = await self._execute(GetExInstrumentCountCmd())
n = 0
page = 1000
pos = offset
while pos < total:
batch = await self._execute(GetExInstrumentInfoCmd(start=pos, count=page))
if not batch:
break
for item in batch:
if item.market == market:
n += 1
elif item.market > market:
return n
pos += page
return n
async def goods_list(self, market: int, start: int = 0, count: int = 600) -> pd.DataFrame:
"""获取扩展市场商品列表(期货合约/港股/美股等)。"""
offset = await self._find_market_offset(market)
if offset < 0:
return pd.DataFrame()
total = await self._execute(GetExInstrumentCountCmd())
page_size = 1000
collected: list[Any] = []
skipped = 0
pos = offset
while pos < total and len(collected) < count:
batch = await self._execute(GetExInstrumentInfoCmd(start=pos, count=page_size))
if not batch:
break
for item in batch:
if item.market == market:
if skipped < start:
skipped += 1
else:
collected.append(item)
if len(collected) >= count:
break
elif item.market > market:
break
else:
pos += page_size
continue
break
return _to_df(collected)
async def _find_market_offset(self, market: int) -> int:
"""二分查找定位指定市场在全局商品列表中的起始偏移。"""
total = await self._execute(GetExInstrumentCountCmd())
if total == 0:
return -1
lo, hi = 0, total
while lo < hi:
mid = (lo + hi) // 2
items = await self._execute(GetExInstrumentInfoCmd(start=mid, count=1))
if not items:
hi = mid
continue
m = items[0].market
if m < market:
lo = mid + 1
else:
hi = mid
return lo
# ------------------------------------------------------------------ #
# 行情
# ------------------------------------------------------------------ #
async def goods_quotes(
self,
stocks: list[tuple[int, str]],
fields: Any = None,
) -> pd.DataFrame:
cmd = SymbolQuotesCmd(stocks, fields)
result: list[MacQuoteField] = await self._execute(cmd)
return _quotes_to_df(result)
async def goods_quotes_list(
self,
market: int,
start: int = 0,
count: int = 100,
sort_type: SortType = SortType.CODE,
sort_order: SortOrder = SortOrder.NONE,
) -> pd.DataFrame:
page_size = min(count, 80)
items_df = await self.goods_list(market, start=start, count=page_size)
if items_df.empty:
return pd.DataFrame()
stocks: list[tuple[int, str]] = [(market, row["code"]) for _, row in items_df.iterrows()]
cmd = SymbolQuotesCmd(stocks)
result: list[MacQuoteField] = await self._execute(cmd)
return _quotes_to_df(result)
# ------------------------------------------------------------------ #
# K 线
# ------------------------------------------------------------------ #
async def goods_kline(
self,
market: int,
code: str,
period: Period = Period.DAILY,
start: int = 0,
count: int = 800,
adjust: Adjust = Adjust.NONE,
) -> pd.DataFrame:
cmd = SymbolBarCmd(
market=market,
code=code,
period=period,
start=start,
count=count,
fq=adjust,
)
result = await self._execute(cmd)
return _to_df(result)
# ------------------------------------------------------------------ #
# 分时
# ------------------------------------------------------------------ #
async def goods_tick_chart(
self,
market: int,
code: str,
query_date: date | None = None,
) -> pd.DataFrame:
cmd = SymbolTickChartCmd(market=market, code=code, query_date=query_date)
result = await self._execute(cmd)
return _to_df(result)
async def goods_chart_sampling(
self,
market: int,
code: str,
) -> pd.DataFrame:
cmd = ChartSamplingCmd(market=market, code=code)
prices: list[float] = await self._execute(cmd)
if not prices:
return pd.DataFrame()
return pd.DataFrame({"price": prices})
# ------------------------------------------------------------------ #
# 成交
# ------------------------------------------------------------------ #
async def goods_transaction(
self,
market: int,
code: str,
query_date: date | None = None,
start: int = 0,
count: int = 2000,
) -> pd.DataFrame:
"""获取逐笔成交数据(异步)。
路由与 ``start`` 倒序语义见同步版 :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,
query_date=query_date,
start=start,
count=count,
)
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)