mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 14:34:18 +08:00
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 通过。
757 lines
25 KiB
Python
757 lines
25 KiB
Python
"""MAC 协议扩展市场高层 API:MacExClient(同步)和 AsyncMacExClient(asyncio)。
|
||
|
||
期货/港股/美股等扩展市场通过 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
|
||
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_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:
|
||
"""执行命令;断线时指数退避重试(4 次,与 A 股/MAC 统一,审计 #2)。
|
||
|
||
每次重连后必须重新 ``_login()``(MAC 协议扩展行情特有)。登录握手期的
|
||
``TdxConnectionError`` 与业务请求一样计入退避重试;``TdxCommandError``
|
||
(登录被拒等确定性失败)不重试,直接抛出。
|
||
"""
|
||
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
|
||
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
|
||
起始偏移。
|
||
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,
|
||
query_date=query_date,
|
||
start=start,
|
||
count=count,
|
||
)
|
||
result = self._execute(cmd)
|
||
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:
|
||
"""执行命令;断线时指数退避重试(4 次,与 A 股/MAC 统一,审计 #2)。
|
||
|
||
每次重连后必须重新 ``_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
|
||
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:
|
||
"""获取逐笔成交数据(异步)。路由说明见同步版 :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)
|