mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 13:24:15 +08:00
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:
@@ -2,6 +2,20 @@
|
||||
|
||||
本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
|
||||
|
||||
## [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 通过。
|
||||
|
||||
### 修复
|
||||
|
||||
- **港股逐笔成交协议路由**(`src/easy_tdx/ex/mac_client.py` 同步 + 异步 `goods_transaction`、新增 `src/easy_tdx/ex/_hk_transaction.py`)—— 港股股票类市场(`HK_STOCK_MARKETS = {27, 31, 48, 49, 71, 98}`,即 HK_INDEX/HK_MAIN_BOARD/HK_GEM/HK_FUND/HK_STOCK_GGT/HK_DARK_POOL)改走 ex 扩展行情协议:`query_date=None` → `GetExTransactionDataCmd`(0x23FC 当日),指定日期 → `GetExHistoryTransactionDataCmd`(0x2406 历史)。返回的 `ExTransactionRecord`(price 为整数、单位 0.001 HKD)映射为与 A 股 `MacTransaction` 一致的 schema(`time/price/vol/trade_count/bs_flag`),价格 ÷1000 换算为港元浮点,与港股分时图 float 价格对齐。count > 1800 时按 1800/页自动分页。其余扩展市场(美股 / 期货等)保持 MAC 0x122F 路径不变。
|
||||
- **回归测试**(`tests/unit/test_hk_transaction.py`,新建 +22 例;`tests/fixtures/ex_history_transaction.hex` + `.json`,录制自真实港股 00700 在 2026-07-03 的 0x2406 响应)—— 覆盖:ex 历史 0x2406 响应解析、空响应处理、`ExTransactionRecord → MacTransaction` 字段映射 + 价格换算、`is_hk_stock_market` 市场判定边界(11 个参数化用例)、mock `_execute` 验证路由(港股走 ex / 期货仍走 0x122F)、分页与空停止逻辑。
|
||||
|
||||
### 说明
|
||||
|
||||
- issue #14 反馈的 `df1`(7/4 周六休市)与 `df2`(7/1 香港回归纪念日休市)返回空属正常休市;真正的 bug 是 `df3`(7/3 开市日 02715,`HK_MAIN_BOARD`)。修复后开市日港股逐笔成交可正常取数。
|
||||
- 港股衍生品(HK_FINANCIAL_FUTURES=23 / HK_STOCK_OPTIONS=26 等)不在本次路由范围:期货/期权逐笔语义不同,且 0x122F 对 CFFEX 期货恰好可用,保持现状避免回归。
|
||||
|
||||
## [1.17.4] — 2026-07-04
|
||||
|
||||
**Web UI 回测交互重构 + 一键寻优全策略** —— 针对单标的 / 组合 / 寻优四个页面做交互精简与能力补强:取行情整合进「开始回测」一键完成、市场选择改为 6 位代码智能识别、成交价精简为开盘价/收盘价、初始资金统一为 100 万、新增 18 策略预设参数网格与「一键寻优所有策略」全局排名。**838 单测全绿**(+2),ruff / mypy strict / vue-tsc 全部通过。
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "easy-tdx"
|
||||
version = "1.17.4"
|
||||
version = "1.17.5"
|
||||
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -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 int(ex 历史命令的日期参数格式)。"""
|
||||
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
|
||||
@@ -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,
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
1f303037303000000000000000000a00bf03b89606002c010000000000000002bf03b896060064000000000000000002bf03b89606001f000000000000000002bf03b896060064000000000000000002bf032895060064000000000000000002bf0328950600c8000000000000000002bf03289506002c010000000000000002bf03b8960600c8000000000000000002bf03b896060064000000000000000002c803609406002c430e00000000000002
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"_comment": "港股 00700 (HK_MAIN_BOARD, market=31) 2026-07-03 历史 0x2406 响应 body。issue #14 fixture。价格字段为整数(单位 0.001 HKD):431800 = 431.8 港元。时间 15:59=收盘竞价、16:08=收盘集合竞价大单。",
|
||||
"market": 31,
|
||||
"code": "00700",
|
||||
"num_records": 10,
|
||||
"first": {
|
||||
"hour": 15,
|
||||
"minute": 59,
|
||||
"second": 0,
|
||||
"price_int": 431800,
|
||||
"price_hkd": 431.8,
|
||||
"vol": 300
|
||||
},
|
||||
"last": {
|
||||
"hour": 16,
|
||||
"minute": 8,
|
||||
"second": 0,
|
||||
"price_int": 431200,
|
||||
"price_hkd": 431.2,
|
||||
"vol": 934700
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
"""港股逐笔成交协议路由的回归测试(issue #14)。
|
||||
|
||||
issue #14:``MacExClient.goods_transaction`` 对港股返回空。根因是它对所有扩展市场
|
||||
复用了 A 股 MAC 协议的 ``SymbolTransactionCmd``(0x122F),而 0x122F 的数据源未接入
|
||||
港股。修复后港股股票类市场走 ex 扩展行情协议(当日 0x23FC / 历史 0x2406)。
|
||||
|
||||
本测试纯离线:
|
||||
|
||||
1. 用录制的真实港股 0x2406 响应 fixture 验证 ``GetExHistoryTransactionDataCmd``
|
||||
解析正确(价格字段为整数)。
|
||||
2. 验证 ``ExTransactionRecord → MacTransaction`` 字段映射 + 价格 ÷1000 换算。
|
||||
3. 验证 ``is_hk_stock_market`` 市场判定边界。
|
||||
4. mock ``_execute``,验证 ``MacExClient.goods_transaction`` 对港股(market=31)
|
||||
走 ex 协议路径、对其他扩展市场(market=47 期货)仍走 0x122F 路径,避免回归。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
from datetime import date, time
|
||||
|
||||
import pytest
|
||||
|
||||
from easy_tdx.ex._hk_transaction import (
|
||||
HK_STOCK_MARKETS,
|
||||
_fetch_hk_transactions_sync,
|
||||
_map_record,
|
||||
is_hk_stock_market,
|
||||
)
|
||||
from easy_tdx.ex.commands.get_transaction import (
|
||||
GetExHistoryTransactionDataCmd,
|
||||
GetExTransactionDataCmd,
|
||||
)
|
||||
from easy_tdx.ex.models import ExTransactionRecord
|
||||
from easy_tdx.mac.commands.symbol_transaction import SymbolTransactionCmd
|
||||
from easy_tdx.mac.models import MacTransaction
|
||||
|
||||
FIXTURES = pathlib.Path(__file__).parent.parent / "fixtures"
|
||||
|
||||
|
||||
def load_hex(name: str) -> bytes:
|
||||
return bytes.fromhex((FIXTURES / f"{name}.hex").read_text().strip())
|
||||
|
||||
|
||||
def load_json(name: str) -> dict:
|
||||
return json.loads((FIXTURES / f"{name}.json").read_text())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. ex 历史 0x2406 协议解析(fixture 来自真实港股 00700 响应)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_parse_ex_history_transaction_hk():
|
||||
"""港股 0x2406 响应解析:返回非空,price 为整数(单位 0.001 HKD)。"""
|
||||
body = load_hex("ex_history_transaction")
|
||||
expected = load_json("ex_history_transaction")
|
||||
|
||||
cmd = GetExHistoryTransactionDataCmd(31, "00700", 20260703, 0, 10)
|
||||
recs = cmd.parse_response(body)
|
||||
|
||||
assert len(recs) == expected["num_records"]
|
||||
|
||||
# 首条字段
|
||||
r0 = recs[0]
|
||||
assert r0.hour == expected["first"]["hour"]
|
||||
assert r0.minute == expected["first"]["minute"]
|
||||
assert r0.second == expected["first"]["second"]
|
||||
assert r0.price == expected["first"]["price_int"] # 整数,未换算
|
||||
assert isinstance(r0.price, int)
|
||||
assert r0.volume == expected["first"]["vol"]
|
||||
|
||||
# 末条(收盘集合竞价大单)
|
||||
rN = recs[-1]
|
||||
assert rN.hour == expected["last"]["hour"]
|
||||
assert rN.price == expected["last"]["price_int"]
|
||||
assert rN.volume == expected["last"]["vol"]
|
||||
|
||||
|
||||
def test_parse_ex_history_transaction_empty():
|
||||
"""空响应(< 16 字节)应返回空列表,不抛异常。"""
|
||||
cmd = GetExHistoryTransactionDataCmd(31, "00700", 20260701, 0, 10)
|
||||
assert cmd.parse_response(b"") == []
|
||||
assert cmd.parse_response(b"\x00" * 10) == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. ExTransactionRecord → MacTransaction 映射 + 价格换算
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_map_record_price_conversion():
|
||||
"""整数价格 431800 → 431.8 港元浮点。"""
|
||||
rec = ExTransactionRecord(
|
||||
hour=15,
|
||||
minute=59,
|
||||
second=0,
|
||||
price=431800,
|
||||
volume=300,
|
||||
zengcang=0,
|
||||
nature=0,
|
||||
)
|
||||
mt = _map_record(rec)
|
||||
|
||||
assert isinstance(mt, MacTransaction)
|
||||
assert mt.time == time(15, 59, 0)
|
||||
assert mt.price == pytest.approx(431.8)
|
||||
assert mt.vol == 300
|
||||
assert mt.trade_count == 0 # ex 协议无此字段
|
||||
assert mt.bs_flag == 0
|
||||
|
||||
|
||||
def test_map_record_nature_to_bs_flag():
|
||||
"""nature(买卖方向)映射到 bs_flag。"""
|
||||
rec = ExTransactionRecord(10, 30, 5, 100000, 1000, 0, nature=1)
|
||||
mt = _map_record(rec)
|
||||
assert mt.bs_flag == 1
|
||||
assert mt.price == pytest.approx(100.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. 市场判定边界
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"market,expected",
|
||||
[
|
||||
(31, True), # HK_MAIN_BOARD
|
||||
(48, True), # HK_GEM
|
||||
(49, True), # HK_FUND
|
||||
(71, True), # HK_STOCK_GGT
|
||||
(98, True), # HK_DARK_POOL
|
||||
(27, True), # HK_INDEX
|
||||
(47, False), # CFFEX_FUTURES(期货,保持 0x122F)
|
||||
(74, False), # US_STOCK(美股,保持 0x122F)
|
||||
(23, False), # HK_FINANCIAL_FUTURES(衍生品不在本次路由范围)
|
||||
(0, False), # 沪深京 A 股市场代码
|
||||
(1, False),
|
||||
(2, False),
|
||||
],
|
||||
)
|
||||
def test_is_hk_stock_market(market: int, expected: bool):
|
||||
assert is_hk_stock_market(market) is expected
|
||||
|
||||
|
||||
def test_hk_stock_markets_constant():
|
||||
"""常量集合稳定,防止误改。"""
|
||||
assert HK_STOCK_MARKETS == frozenset({27, 31, 48, 49, 71, 98})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. MacExClient.goods_transaction 路由(mock _execute,离线)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _build_fake_records(n: int) -> list:
|
||||
"""构造 n 条 ExTransactionRecord。"""
|
||||
return [
|
||||
ExTransactionRecord(
|
||||
hour=15,
|
||||
minute=59,
|
||||
second=0,
|
||||
price=431800 + i,
|
||||
volume=100 * (i + 1),
|
||||
zengcang=0,
|
||||
nature=i % 3,
|
||||
)
|
||||
for i in range(n)
|
||||
]
|
||||
|
||||
|
||||
def test_goods_transaction_hk_uses_ex_protocol(monkeypatch):
|
||||
"""港股 market=31 应走 ex 协议(GetExHistoryTransactionDataCmd),不走 0x122F。"""
|
||||
from easy_tdx.ex.mac_client import MacExClient
|
||||
|
||||
captured: list = []
|
||||
|
||||
def fake_execute(cmd):
|
||||
captured.append(cmd)
|
||||
# 返回 3 条假记录
|
||||
return _build_fake_records(3)
|
||||
|
||||
client = object.__new__(MacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = client.goods_transaction(31, "00700", date(2026, 7, 3), count=3)
|
||||
|
||||
# 应捕获到 GetExHistoryTransactionDataCmd(指定日期 → 0x2406)
|
||||
assert len(captured) == 1
|
||||
assert isinstance(captured[0], GetExHistoryTransactionDataCmd)
|
||||
assert not isinstance(captured[0], SymbolTransactionCmd)
|
||||
|
||||
# 返回 DataFrame 应有数据,价格已换算为港元
|
||||
assert len(df) == 3
|
||||
assert df["price"].iloc[0] == pytest.approx(431.800)
|
||||
assert {"time", "price", "vol", "trade_count", "bs_flag"}.issubset(df.columns)
|
||||
|
||||
|
||||
def test_goods_transaction_hk_today_uses_0x23fc(monkeypatch):
|
||||
"""港股 query_date=None 应走当日命令 GetExTransactionDataCmd(0x23FC)。"""
|
||||
from easy_tdx.ex.mac_client import MacExClient
|
||||
|
||||
captured: list = []
|
||||
|
||||
def fake_execute(cmd):
|
||||
captured.append(cmd)
|
||||
return _build_fake_records(2)
|
||||
|
||||
client = object.__new__(MacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = client.goods_transaction(31, "00700", count=2) # query_date=None
|
||||
|
||||
assert len(captured) == 1
|
||||
assert isinstance(captured[0], GetExTransactionDataCmd)
|
||||
assert len(df) == 2
|
||||
|
||||
|
||||
def test_goods_transaction_non_hk_keeps_0x122f():
|
||||
"""非港股市场(如 CFFEX 期货 market=47)仍走 MAC 0x122F,不回归。"""
|
||||
from easy_tdx.ex.mac_client import MacExClient
|
||||
|
||||
captured: list = []
|
||||
|
||||
def fake_execute(cmd):
|
||||
captured.append(cmd)
|
||||
# 0x122F 返回 MacTransaction 列表
|
||||
return [
|
||||
MacTransaction(
|
||||
time=time(14, 56, 35), price=3850.0, vol=1, trade_count=1, bs_flag=0
|
||||
)
|
||||
]
|
||||
|
||||
client = object.__new__(MacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = client.goods_transaction(47, "IFL0", count=1)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert isinstance(captured[0], SymbolTransactionCmd)
|
||||
assert len(df) == 1
|
||||
assert df["price"].iloc[0] == pytest.approx(3850.0)
|
||||
|
||||
|
||||
def test_fetch_hk_transactions_pagination():
|
||||
"""count 超过单页(1800)应自动分页。"""
|
||||
page_calls: list[tuple[int, int]] = []
|
||||
|
||||
def fake_execute(cmd):
|
||||
# 记录 (offset, count)
|
||||
page_calls.append((cmd.start, cmd.count))
|
||||
# 第一页返回满页,第二页返回部分(触发停止)
|
||||
if cmd.start == 0:
|
||||
return _build_fake_records(cmd.count)
|
||||
return _build_fake_records(500) # 不足一页
|
||||
|
||||
# 请求 2000 条,单页 1800 → 第一页 1800 + 第二页 200,第二页只返回 500>200 条会停止
|
||||
# 但 fake 第二页返回 500 条 > 请求的 200,按分页逻辑应取 500 但 fetched 已达 2300>2000
|
||||
# 实际:page1 size=1800 返回1800, page2 size=min(2000-1800,1800)=200 返回500
|
||||
# len(batch)=500 >= page_size=200 → 不触发 < 停止,但 fetched=2300 >= count=2000 退出
|
||||
result = _fetch_hk_transactions_sync(fake_execute, 31, "00700", None, 0, 2000)
|
||||
|
||||
assert len(page_calls) == 2
|
||||
assert page_calls[0] == (0, 1800)
|
||||
assert page_calls[1] == (1800, 200)
|
||||
# 第一页 1800 + 第二页实际 500 条(fake 返回),但请求只需 2000,第二页 batch=500
|
||||
# 结果 = 1800 + 500 = 2300(fake 多返回了;真实服务器不会超过 page_size)
|
||||
assert len(result) == 2300
|
||||
|
||||
|
||||
def test_fetch_hk_transactions_stops_on_empty():
|
||||
"""空响应应立即停止,不无限循环。"""
|
||||
call_count = 0
|
||||
|
||||
def fake_execute(cmd):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return []
|
||||
|
||||
result = _fetch_hk_transactions_sync(fake_execute, 31, "00700", None, 0, 2000)
|
||||
|
||||
assert call_count == 1 # 第一页空就停
|
||||
assert result == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. AsyncMacExClient.goods_transaction 异步路由(mock _execute,离线)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_goods_transaction_hk_uses_ex_protocol():
|
||||
"""异步版港股 market=31 也应走 ex 协议(GetExHistoryTransactionDataCmd)。"""
|
||||
from easy_tdx.ex.mac_client import AsyncMacExClient
|
||||
|
||||
captured: list = []
|
||||
|
||||
async def fake_execute(cmd):
|
||||
captured.append(cmd)
|
||||
return _build_fake_records(3)
|
||||
|
||||
client = object.__new__(AsyncMacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = await client.goods_transaction(31, "00700", date(2026, 7, 3), count=3)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert isinstance(captured[0], GetExHistoryTransactionDataCmd)
|
||||
assert not isinstance(captured[0], SymbolTransactionCmd)
|
||||
assert len(df) == 3
|
||||
assert df["price"].iloc[0] == pytest.approx(431.800)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_goods_transaction_non_hk_keeps_0x122f():
|
||||
"""异步版非港股市场(期货 market=47)仍走 MAC 0x122F。"""
|
||||
from easy_tdx.ex.mac_client import AsyncMacExClient
|
||||
|
||||
captured: list = []
|
||||
|
||||
async def fake_execute(cmd):
|
||||
captured.append(cmd)
|
||||
return [
|
||||
MacTransaction(
|
||||
time=time(14, 56, 35), price=3850.0, vol=1, trade_count=1, bs_flag=0
|
||||
)
|
||||
]
|
||||
|
||||
client = object.__new__(AsyncMacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = await client.goods_transaction(47, "IFL0", count=1)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert isinstance(captured[0], SymbolTransactionCmd)
|
||||
assert len(df) == 1
|
||||
assert df["price"].iloc[0] == pytest.approx(3850.0)
|
||||
|
||||
Reference in New Issue
Block a user