mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 19:14:19 +08:00
经三轮代码审计后的综合质量加固版本,覆盖协议核心层、数据正确性、 错误处理、测试真实度与可维护性。761 单测全绿(+58),ruff/mypy 全过。 主要修复: - 离线 .day 写入原子化(fsync + _repair_tail + 读取校验,CQS 守住) - 回测止损前视偏差(延迟下一根开盘 + 跳空保护) - VWAP 权重索引 / bar_time fail-fast / 绩效除零保护 - 闭包绑定 / 路径穿越 / naive datetime 跨时区 / ruff UP038 重构: - 抽 AsyncHeartbeatMixin 收敛 4 处心跳副本(12→1) - 统一 _RETRY_DELAYS 退避序列 / scanner 失败可观测性 新增 5 个测试文件 + 公共 API 类型契约,CI 加 Windows 矩阵 + trusted publishing 签名 + 锁文件。 详见 CHANGELOG.md
649 lines
20 KiB
Python
649 lines
20 KiB
Python
"""统一通达信客户端 -- 自动路由 A 股 / 扩展市场。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import TracebackType
|
|
from typing import Any
|
|
|
|
import pandas as pd
|
|
|
|
from .ex.mac_client import AsyncMacExClient, MacExClient
|
|
from .mac.client import AsyncMacClient, MacClient
|
|
from .mac.enums import (
|
|
Adjust,
|
|
BoardType,
|
|
Category,
|
|
FilterType,
|
|
Period,
|
|
SortOrder,
|
|
SortType,
|
|
)
|
|
|
|
|
|
class UnifiedTdxClient:
|
|
"""统一通达信行情客户端。
|
|
|
|
自动路由:A 股方法代理到 MacClient,扩展市场方法代理到 MacExClient。
|
|
MacClient 在 connect()/__enter__ 时立即连接;MacExClient 延迟到首次使用。
|
|
|
|
用法::
|
|
|
|
with UnifiedTdxClient() as client:
|
|
df = client.get_stock_kline(0, "600000", Period.DAILY, count=10)
|
|
df2 = client.goods_kline(ExMarket.US_STOCK, "TSLA", Period.DAILY, count=10)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
heartbeat_interval: float = 15.0,
|
|
timeout: float = 15.0,
|
|
) -> None:
|
|
self._heartbeat_interval = heartbeat_interval
|
|
self._timeout = timeout
|
|
self._mac: MacClient | None = None
|
|
self._mac_ex: MacExClient | None = None
|
|
|
|
def connect(self) -> None:
|
|
self._ensure_mac()
|
|
|
|
def close(self) -> None:
|
|
if self._mac is not None:
|
|
self._mac.close()
|
|
self._mac = None
|
|
if self._mac_ex is not None:
|
|
self._mac_ex.close()
|
|
self._mac_ex = None
|
|
|
|
def disconnect(self) -> None:
|
|
self.close()
|
|
|
|
def __enter__(self) -> UnifiedTdxClient:
|
|
self.connect()
|
|
return self
|
|
|
|
def __exit__(
|
|
self,
|
|
exc_type: type[BaseException] | None,
|
|
exc_val: BaseException | None,
|
|
exc_tb: TracebackType | None,
|
|
) -> None:
|
|
self.close()
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 内部路由
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def _ensure_mac(self) -> MacClient:
|
|
if self._mac is None:
|
|
self._mac = MacClient.from_best_host(
|
|
heartbeat_interval=self._heartbeat_interval,
|
|
timeout=self._timeout,
|
|
)
|
|
self._mac.connect()
|
|
return self._mac
|
|
|
|
def _ensure_mac_ex(self) -> MacExClient:
|
|
if self._mac_ex is None:
|
|
self._mac_ex = MacExClient.from_best_host(timeout=self._timeout)
|
|
self._mac_ex.connect()
|
|
return self._mac_ex
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# A 股方法 (proxy to MacClient)
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def get_stock_quotes(
|
|
self,
|
|
stocks: list[tuple[int, str]],
|
|
fields: Any = None,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_stock_quotes(stocks, fields)
|
|
|
|
def get_stock_quotes_list(
|
|
self,
|
|
category: Category,
|
|
start: int = 0,
|
|
count: int = 80,
|
|
sort_type: SortType = SortType.CHANGE_PCT,
|
|
sort_order: SortOrder = SortOrder.DESC,
|
|
exclude_flags: list[FilterType] | None = None,
|
|
fields: Any = None,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_stock_quotes_list(
|
|
category, start, count, sort_type, sort_order, exclude_flags, fields
|
|
)
|
|
|
|
def get_stock_kline(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
period: Period = Period.DAILY,
|
|
start: int = 0,
|
|
count: int = 800,
|
|
times: int = 1,
|
|
adjust: Adjust = Adjust.NONE,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_stock_kline(market, code, period, start, count, times, adjust)
|
|
|
|
def get_stock_kline_with_indicators(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
indicators: list[str],
|
|
period: Period = Period.DAILY,
|
|
count: int = 30,
|
|
adjust: Adjust = Adjust.QFQ,
|
|
params: dict[str, dict[str, int | float]] | None = None,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_stock_kline_with_indicators(
|
|
market,
|
|
code,
|
|
indicators,
|
|
period,
|
|
count,
|
|
adjust,
|
|
params,
|
|
)
|
|
|
|
def get_tick_chart(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
date: int | None = None,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_tick_chart(market, code, date)
|
|
|
|
def get_tick_charts(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
date: int | None = None,
|
|
days: int = 5,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_tick_charts(market, code, date, days)
|
|
|
|
def get_chart_sampling(self, market: int, code: str) -> pd.DataFrame:
|
|
return self._ensure_mac().get_chart_sampling(market, code)
|
|
|
|
def get_transactions(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
count: int = 2000,
|
|
start: int = 0,
|
|
date: int | None = None,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_transactions(market, code, count, start, date)
|
|
|
|
def get_symbol_info(self, market: int, code: str) -> pd.DataFrame:
|
|
return self._ensure_mac().get_symbol_info(market, code)
|
|
|
|
def get_board_list(
|
|
self,
|
|
board_type: BoardType = BoardType.ALL,
|
|
count: int = 10000,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_board_list(board_type, count)
|
|
|
|
def get_board_members(
|
|
self,
|
|
board_symbol: str,
|
|
count: int = 100000,
|
|
sort_type: SortType = SortType.CHANGE_PCT,
|
|
sort_order: SortOrder = SortOrder.DESC,
|
|
fields: Any = None,
|
|
exclude_flags: list[FilterType] | None = None,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_board_members(
|
|
board_symbol, count, sort_type, sort_order, fields, exclude_flags
|
|
)
|
|
|
|
def get_belong_board(self, market: int, code: str) -> pd.DataFrame:
|
|
return self._ensure_mac().get_belong_board(market, code)
|
|
|
|
def get_capital_flow(self, market: int, code: str) -> pd.DataFrame:
|
|
return self._ensure_mac().get_capital_flow(market, code)
|
|
|
|
def get_auction(self, market: int, code: str) -> pd.DataFrame:
|
|
return self._ensure_mac().get_auction(market, code)
|
|
|
|
def get_unusual(
|
|
self,
|
|
market: int,
|
|
start: int = 0,
|
|
count: int = 0,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_unusual(market, start, count)
|
|
|
|
def get_server_info(self) -> pd.DataFrame:
|
|
return self._ensure_mac().get_server_info()
|
|
|
|
def get_kline_offset(
|
|
self,
|
|
offset: int = 0,
|
|
count: int = 128000,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac().get_kline_offset(offset, count)
|
|
|
|
def get_file_meta(self, filename: str) -> pd.DataFrame:
|
|
return self._ensure_mac().get_file_meta(filename)
|
|
|
|
def download_file_chunk(
|
|
self,
|
|
filename: str,
|
|
index: int,
|
|
offset: int,
|
|
size: int,
|
|
) -> bytes:
|
|
return self._ensure_mac().download_file_chunk(filename, index, offset, size)
|
|
|
|
def download_file(
|
|
self,
|
|
filename: str,
|
|
filesize: int = 0,
|
|
) -> bytearray:
|
|
return self._ensure_mac().download_file(filename, filesize)
|
|
|
|
def get_goods_list(
|
|
self,
|
|
market: int,
|
|
start: int = 0,
|
|
count: int = 600,
|
|
) -> pd.DataFrame:
|
|
""".. deprecated:: 1.16.2
|
|
与 :meth:`goods_list` 完全相同(历史遗留的重复代理方法)。
|
|
请改用 :meth:`goods_list`(与底层 ``MacExClient`` 命名一致)。
|
|
"""
|
|
import warnings
|
|
|
|
warnings.warn(
|
|
"get_goods_list 是重复方法,请改用 goods_list", DeprecationWarning, stacklevel=2
|
|
)
|
|
return self._ensure_mac_ex().goods_list(market, start, count)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 扩展市场方法 (proxy to MacExClient)
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def goods_count(self, market: int) -> int:
|
|
return self._ensure_mac_ex().goods_count(market)
|
|
|
|
def goods_list(self, market: int, start: int = 0, count: int = 600) -> pd.DataFrame:
|
|
return self._ensure_mac_ex().goods_list(market, start, count)
|
|
|
|
def goods_quotes(
|
|
self,
|
|
stocks: list[tuple[int, str]],
|
|
fields: Any = None,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac_ex().goods_quotes(stocks, fields)
|
|
|
|
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:
|
|
return self._ensure_mac_ex().goods_quotes_list(market, start, count, sort_type, sort_order)
|
|
|
|
def goods_kline(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
period: Period = Period.DAILY,
|
|
start: int = 0,
|
|
count: int = 800,
|
|
adjust: Adjust = Adjust.NONE,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac_ex().goods_kline(market, code, period, start, count, adjust)
|
|
|
|
def goods_tick_chart(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
query_date: object = None,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac_ex().goods_tick_chart(market, code, query_date) # type: ignore[arg-type]
|
|
|
|
def goods_chart_sampling(self, market: int, code: str) -> pd.DataFrame:
|
|
return self._ensure_mac_ex().goods_chart_sampling(market, code)
|
|
|
|
def goods_transaction(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
query_date: object = None,
|
|
start: int = 0,
|
|
count: int = 2000,
|
|
) -> pd.DataFrame:
|
|
return self._ensure_mac_ex().goods_transaction(market, code, query_date, start, count) # type: ignore[arg-type]
|
|
|
|
|
|
class AsyncUnifiedTdxClient:
|
|
"""异步统一通达信行情客户端。
|
|
|
|
用法::
|
|
|
|
async with AsyncUnifiedTdxClient() as client:
|
|
df = await client.get_stock_kline(0, "600000", Period.DAILY, count=10)
|
|
df2 = await client.goods_kline(ExMarket.US_STOCK, "TSLA", Period.DAILY, count=10)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
heartbeat_interval: float = 15.0,
|
|
timeout: float = 15.0,
|
|
) -> None:
|
|
self._heartbeat_interval = heartbeat_interval
|
|
self._timeout = timeout
|
|
self._mac: AsyncMacClient | None = None
|
|
self._mac_ex: AsyncMacExClient | None = None
|
|
|
|
async def connect(self) -> None:
|
|
await self._ensure_mac()
|
|
|
|
async def close(self) -> None:
|
|
if self._mac is not None:
|
|
await self._mac.close()
|
|
self._mac = None
|
|
if self._mac_ex is not None:
|
|
await self._mac_ex.close()
|
|
self._mac_ex = None
|
|
|
|
async def disconnect(self) -> None:
|
|
await self.close()
|
|
|
|
async def __aenter__(self) -> AsyncUnifiedTdxClient:
|
|
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()
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 内部路由
|
|
# ------------------------------------------------------------------ #
|
|
|
|
async def _ensure_mac(self) -> AsyncMacClient:
|
|
if self._mac is None:
|
|
self._mac = AsyncMacClient.from_best_host(
|
|
heartbeat_interval=self._heartbeat_interval,
|
|
timeout=self._timeout,
|
|
)
|
|
await self._mac.connect()
|
|
return self._mac
|
|
|
|
async def _ensure_mac_ex(self) -> AsyncMacExClient:
|
|
if self._mac_ex is None:
|
|
self._mac_ex = AsyncMacExClient.from_best_host(timeout=self._timeout)
|
|
await self._mac_ex.connect()
|
|
return self._mac_ex
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# A 股方法 (proxy to AsyncMacClient)
|
|
# ------------------------------------------------------------------ #
|
|
|
|
async def get_stock_quotes(
|
|
self,
|
|
stocks: list[tuple[int, str]],
|
|
fields: Any = None,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_stock_quotes(stocks, fields)
|
|
|
|
async def get_stock_quotes_list(
|
|
self,
|
|
category: Category,
|
|
start: int = 0,
|
|
count: int = 80,
|
|
sort_type: SortType = SortType.CHANGE_PCT,
|
|
sort_order: SortOrder = SortOrder.DESC,
|
|
exclude_flags: list[FilterType] | None = None,
|
|
fields: Any = None,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_stock_quotes_list(
|
|
category, start, count, sort_type, sort_order, exclude_flags, fields
|
|
)
|
|
|
|
async def get_stock_kline(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
period: Period = Period.DAILY,
|
|
start: int = 0,
|
|
count: int = 800,
|
|
times: int = 1,
|
|
adjust: Adjust = Adjust.NONE,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_stock_kline(market, code, period, start, count, times, adjust)
|
|
|
|
async def get_stock_kline_with_indicators(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
indicators: list[str],
|
|
period: Period = Period.DAILY,
|
|
count: int = 30,
|
|
adjust: Adjust = Adjust.QFQ,
|
|
params: dict[str, dict[str, int | float]] | None = None,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_stock_kline_with_indicators(
|
|
market,
|
|
code,
|
|
indicators,
|
|
period,
|
|
count,
|
|
adjust,
|
|
params,
|
|
)
|
|
|
|
async def get_tick_chart(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
date: int | None = None,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_tick_chart(market, code, date)
|
|
|
|
async def get_tick_charts(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
date: int | None = None,
|
|
days: int = 5,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_tick_charts(market, code, date, days)
|
|
|
|
async def get_chart_sampling(self, market: int, code: str) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_chart_sampling(market, code)
|
|
|
|
async def get_transactions(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
count: int = 2000,
|
|
start: int = 0,
|
|
date: int | None = None,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_transactions(market, code, count, start, date)
|
|
|
|
async def get_symbol_info(self, market: int, code: str) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_symbol_info(market, code)
|
|
|
|
async def get_board_list(
|
|
self,
|
|
board_type: BoardType = BoardType.ALL,
|
|
count: int = 10000,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_board_list(board_type, count)
|
|
|
|
async def get_board_members(
|
|
self,
|
|
board_symbol: str,
|
|
count: int = 100000,
|
|
sort_type: SortType = SortType.CHANGE_PCT,
|
|
sort_order: SortOrder = SortOrder.DESC,
|
|
fields: Any = None,
|
|
exclude_flags: list[FilterType] | None = None,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_board_members(
|
|
board_symbol, count, sort_type, sort_order, fields, exclude_flags
|
|
)
|
|
|
|
async def get_belong_board(self, market: int, code: str) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_belong_board(market, code)
|
|
|
|
async def get_capital_flow(self, market: int, code: str) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_capital_flow(market, code)
|
|
|
|
async def get_auction(self, market: int, code: str) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_auction(market, code)
|
|
|
|
async def get_unusual(
|
|
self,
|
|
market: int,
|
|
start: int = 0,
|
|
count: int = 0,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_unusual(market, start, count)
|
|
|
|
async def get_server_info(self) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_server_info()
|
|
|
|
async def get_kline_offset(
|
|
self,
|
|
offset: int = 0,
|
|
count: int = 128000,
|
|
) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_kline_offset(offset, count)
|
|
|
|
async def get_file_meta(self, filename: str) -> pd.DataFrame:
|
|
mac = await self._ensure_mac()
|
|
return await mac.get_file_meta(filename)
|
|
|
|
async def download_file_chunk(
|
|
self,
|
|
filename: str,
|
|
index: int,
|
|
offset: int,
|
|
size: int,
|
|
) -> bytes:
|
|
mac = await self._ensure_mac()
|
|
return await mac.download_file_chunk(filename, index, offset, size)
|
|
|
|
async def download_file(
|
|
self,
|
|
filename: str,
|
|
filesize: int = 0,
|
|
) -> bytearray:
|
|
mac = await self._ensure_mac()
|
|
return await mac.download_file(filename, filesize)
|
|
|
|
async def get_goods_list(
|
|
self,
|
|
market: int,
|
|
start: int = 0,
|
|
count: int = 600,
|
|
) -> pd.DataFrame:
|
|
""".. deprecated:: 1.16.2
|
|
与 :meth:`goods_list` 完全相同(历史遗留的重复代理方法)。
|
|
请改用 :meth:`goods_list`(与底层 ``MacExClient`` 命名一致)。
|
|
"""
|
|
import warnings
|
|
|
|
warnings.warn(
|
|
"get_goods_list 是重复方法,请改用 goods_list", DeprecationWarning, stacklevel=2
|
|
)
|
|
ex = await self._ensure_mac_ex()
|
|
return await ex.goods_list(market, start, count)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 扩展市场方法 (proxy to AsyncMacExClient)
|
|
# ------------------------------------------------------------------ #
|
|
|
|
async def goods_count(self, market: int) -> int:
|
|
ex = await self._ensure_mac_ex()
|
|
return await ex.goods_count(market)
|
|
|
|
async def goods_list(self, market: int, start: int = 0, count: int = 600) -> pd.DataFrame:
|
|
ex = await self._ensure_mac_ex()
|
|
return await ex.goods_list(market, start, count)
|
|
|
|
async def goods_quotes(
|
|
self,
|
|
stocks: list[tuple[int, str]],
|
|
fields: Any = None,
|
|
) -> pd.DataFrame:
|
|
ex = await self._ensure_mac_ex()
|
|
return await ex.goods_quotes(stocks, fields)
|
|
|
|
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:
|
|
ex = await self._ensure_mac_ex()
|
|
return await ex.goods_quotes_list(market, start, count, sort_type, sort_order)
|
|
|
|
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:
|
|
ex = await self._ensure_mac_ex()
|
|
return await ex.goods_kline(market, code, period, start, count, adjust)
|
|
|
|
async def goods_tick_chart(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
query_date: object = None,
|
|
) -> pd.DataFrame:
|
|
ex = await self._ensure_mac_ex()
|
|
return await ex.goods_tick_chart(market, code, query_date) # type: ignore[arg-type]
|
|
|
|
async def goods_chart_sampling(self, market: int, code: str) -> pd.DataFrame:
|
|
ex = await self._ensure_mac_ex()
|
|
return await ex.goods_chart_sampling(market, code)
|
|
|
|
async def goods_transaction(
|
|
self,
|
|
market: int,
|
|
code: str,
|
|
query_date: object = None,
|
|
start: int = 0,
|
|
count: int = 2000,
|
|
) -> pd.DataFrame:
|
|
ex = await self._ensure_mac_ex()
|
|
return await ex.goods_transaction(market, code, query_date, start, count) # type: ignore[arg-type]
|