mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 16:54:20 +08:00
fix(ex): ex tick --date 接受 YYYYMMDD 整数,修复 AttributeError
This commit is contained in:
@@ -26,5 +26,9 @@ src/easy_tdx/exchange_margin.py
|
||||
# 本地文件,不递交到 GitHub
|
||||
CLAUDE.md
|
||||
|
||||
# 本机包管理缓存(沙箱环境下避开 AppData 路径,落在项目内)
|
||||
.npm-cache/
|
||||
.uv-cache/
|
||||
|
||||
# 代码审计报告(本地产物,不入库)
|
||||
audit-report-*.html
|
||||
|
||||
@@ -150,6 +150,8 @@ def tick(
|
||||
easy-tdx ex tick HK_MAIN_BOARD 00700
|
||||
|
||||
easy-tdx ex tick US_STOCK AAPL --table
|
||||
|
||||
easy-tdx ex tick US_STOCK TSLA --date 20260827 --table
|
||||
"""
|
||||
from .conn import get_mac_ex_client
|
||||
from .output import print_output
|
||||
@@ -158,7 +160,7 @@ def tick(
|
||||
fmt = "table" if use_table else output_fmt
|
||||
mkt = parse_ex_market(market)
|
||||
with get_mac_ex_client() as client:
|
||||
df = client.goods_tick_chart(mkt, code, query_date=date) # type: ignore[arg-type]
|
||||
df = client.goods_tick_chart(mkt, code, query_date=date)
|
||||
print_output(df, fmt)
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,17 @@ _T = TypeVar("_T")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _coerce_query_date(query_date: int | date | None) -> date | None:
|
||||
"""把查询日期统一为 date 对象。
|
||||
|
||||
兼容两种调用方式(与 A 股 MacClient.get_tick_chart 的 YYYYMMDD 整数
|
||||
语义保持一致):int 视为 YYYYMMDD,date 原样透传,None 表示今天。
|
||||
"""
|
||||
if query_date is None or isinstance(query_date, date):
|
||||
return query_date
|
||||
return date(query_date // 10000, (query_date % 10000) // 100, query_date % 100)
|
||||
|
||||
|
||||
def _quotes_to_df(result: list[MacQuoteField]) -> pd.DataFrame:
|
||||
"""将 MacQuoteField 列表展开为 DataFrame。"""
|
||||
rows: list[dict[str, Any]] = []
|
||||
@@ -408,7 +419,7 @@ class MacExClient:
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
query_date: date | None = None,
|
||||
query_date: int | date | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""获取单日分时图。
|
||||
|
||||
@@ -418,10 +429,12 @@ class MacExClient:
|
||||
ExMarket 枚举值。
|
||||
code : str
|
||||
证券代码。
|
||||
query_date : date | None
|
||||
查询日期,None 表示今天。
|
||||
query_date : int | date | None
|
||||
查询日期:date 对象或 YYYYMMDD 整数,None 表示今天。
|
||||
"""
|
||||
cmd = SymbolTickChartCmd(market=market, code=code, query_date=query_date)
|
||||
cmd = SymbolTickChartCmd(
|
||||
market=market, code=code, query_date=_coerce_query_date(query_date)
|
||||
)
|
||||
result = self._execute(cmd)
|
||||
return _to_df(result)
|
||||
|
||||
@@ -453,7 +466,7 @@ class MacExClient:
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
query_date: date | None = None,
|
||||
query_date: int | date | None = None,
|
||||
start: int = 0,
|
||||
count: int = 2000,
|
||||
) -> pd.DataFrame:
|
||||
@@ -486,13 +499,13 @@ class MacExClient:
|
||||
"""
|
||||
if is_hk_stock_market(market):
|
||||
result = _fetch_hk_transactions_sync(
|
||||
self._execute, market, code, query_date, start, count
|
||||
self._execute, market, code, _coerce_query_date(query_date), start, count
|
||||
)
|
||||
return _to_df(result)
|
||||
cmd = SymbolTransactionCmd(
|
||||
market=market,
|
||||
code=code,
|
||||
query_date=query_date,
|
||||
query_date=_coerce_query_date(query_date),
|
||||
start=start,
|
||||
count=count,
|
||||
)
|
||||
@@ -824,9 +837,11 @@ class AsyncMacExClient(AsyncHeartbeatMixin):
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
query_date: date | None = None,
|
||||
query_date: int | date | None = None,
|
||||
) -> pd.DataFrame:
|
||||
cmd = SymbolTickChartCmd(market=market, code=code, query_date=query_date)
|
||||
cmd = SymbolTickChartCmd(
|
||||
market=market, code=code, query_date=_coerce_query_date(query_date)
|
||||
)
|
||||
result = await self._execute(cmd)
|
||||
return _to_df(result)
|
||||
|
||||
@@ -849,7 +864,7 @@ class AsyncMacExClient(AsyncHeartbeatMixin):
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
query_date: date | None = None,
|
||||
query_date: int | date | None = None,
|
||||
start: int = 0,
|
||||
count: int = 2000,
|
||||
) -> pd.DataFrame:
|
||||
@@ -859,13 +874,13 @@ class AsyncMacExClient(AsyncHeartbeatMixin):
|
||||
"""
|
||||
if is_hk_stock_market(market):
|
||||
result = await _fetch_hk_transactions_async(
|
||||
self._execute, market, code, query_date, start, count
|
||||
self._execute, market, code, _coerce_query_date(query_date), start, count
|
||||
)
|
||||
return _to_df(result)
|
||||
cmd = SymbolTransactionCmd(
|
||||
market=market,
|
||||
code=code,
|
||||
query_date=query_date,
|
||||
query_date=_coerce_query_date(query_date),
|
||||
start=start,
|
||||
count=count,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
"""ex 分时/成交查询日期类型的回归测试。
|
||||
|
||||
issue 修复:'easy-tdx ex tick US_STOCK TSLA --date 20260827' 曾因 CLI 传入
|
||||
'int'(YYYYMMDD)而 goods_tick_chart 只接受 datetime.date,直接抛
|
||||
AttributeError: 'int' object has no attribute 'year'(cmd_ex.py 里还遗留
|
||||
type: ignore[arg-type])。
|
||||
|
||||
修复方式:MacExClient / AsyncMacExClient 的 goods_tick_chart /
|
||||
goods_transaction 统一接受 int(YYYYMMDD)/ date / None,内部经
|
||||
_coerce_query_date 归一为 date,与 A 股 MacClient.get_tick_chart 的
|
||||
YYYYMMDD 整数语义保持一致。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from easy_tdx.ex.mac_client import MacExClient, _coerce_query_date
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. _coerce_query_date 纯函数
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw,expected",
|
||||
[
|
||||
(20260827, date(2026, 8, 27)),
|
||||
(20250101, date(2025, 1, 1)),
|
||||
(20260103, date(2026, 1, 3)), # 月份/日期前导零按整数解析
|
||||
(date(2026, 8, 27), date(2026, 8, 27)), # date 原样透传
|
||||
(None, None),
|
||||
],
|
||||
)
|
||||
def test_coerce_query_date(raw: int | date | None, expected: date | None) -> None:
|
||||
assert _coerce_query_date(raw) == expected
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. MacExClient 同步客户端
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _fake_sync_client(captured: list) -> MacExClient:
|
||||
"""构造不联网的 MacExClient(仅替换 _execute,记录命令)。"""
|
||||
|
||||
def fake_execute(cmd):
|
||||
captured.append(cmd)
|
||||
return []
|
||||
|
||||
client = object.__new__(MacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
return client
|
||||
|
||||
|
||||
def test_goods_tick_chart_int_date() -> None:
|
||||
"""int YYYYMMDD 应被转换为 date 后传给 SymbolTickChartCmd。"""
|
||||
from easy_tdx.mac.models import MacTickChart
|
||||
|
||||
captured: list = []
|
||||
|
||||
def fake_execute(cmd):
|
||||
captured.append(cmd)
|
||||
return MacTickChart(
|
||||
market=74,
|
||||
code="TSLA",
|
||||
name="Tesla",
|
||||
pre_close=0.0,
|
||||
open=0.0,
|
||||
high=0.0,
|
||||
low=0.0,
|
||||
close=0.0,
|
||||
vol=0,
|
||||
amount=0.0,
|
||||
turnover=0.0,
|
||||
avg=0.0,
|
||||
charts=[],
|
||||
)
|
||||
|
||||
client = object.__new__(MacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = client.goods_tick_chart(74, "TSLA", query_date=20260827)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert captured[0]._ymd == 20260827 # 归一为 date 后再编码为 YYYYMMDD
|
||||
assert len(df) == 1 and df.iloc[0]["code"] == "TSLA" # 正常转 DataFrame
|
||||
|
||||
|
||||
def test_goods_tick_chart_date_and_none() -> None:
|
||||
"""date 对象与 None 保持原语义(不回归)。"""
|
||||
captured: list = []
|
||||
client = _fake_sync_client(captured)
|
||||
|
||||
client.goods_tick_chart(74, "TSLA", query_date=date(2026, 8, 27))
|
||||
assert captured[-1]._ymd == 20260827
|
||||
|
||||
client.goods_tick_chart(74, "TSLA")
|
||||
assert captured[-1]._ymd == 0 # None → 今天(协议 0)
|
||||
|
||||
|
||||
def test_goods_transaction_int_date_non_hk() -> None:
|
||||
"""非港股市场(美股 74):int 日期应转换后传给 SymbolTransactionCmd(0x122F)。"""
|
||||
captured: list = []
|
||||
client = _fake_sync_client(captured)
|
||||
|
||||
client.goods_transaction(74, "TSLA", query_date=20260827, count=10)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert captured[0]._ymd == 20260827
|
||||
|
||||
|
||||
def test_goods_transaction_int_date_hk() -> None:
|
||||
"""港股市场(31):int 日期应转换后走 ex 历史逐笔协议(GetExHistoryTransactionDataCmd)。"""
|
||||
from easy_tdx.ex.commands.get_transaction import GetExHistoryTransactionDataCmd
|
||||
|
||||
captured: list = []
|
||||
client = _fake_sync_client(captured)
|
||||
|
||||
client.goods_transaction(31, "00700", query_date=20260827, count=100)
|
||||
|
||||
assert captured
|
||||
assert all(isinstance(c, GetExHistoryTransactionDataCmd) for c in captured)
|
||||
assert captured[0].date == 20260827
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. AsyncMacExClient 异步客户端
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_async_goods_tick_chart_int_date() -> None:
|
||||
"""异步版同样接受 int YYYYMMDD。"""
|
||||
from easy_tdx.ex.mac_client import AsyncMacExClient
|
||||
from easy_tdx.mac.models import MacTickChart
|
||||
|
||||
captured: list = []
|
||||
|
||||
async def fake_execute(cmd):
|
||||
captured.append(cmd)
|
||||
return MacTickChart(
|
||||
market=74,
|
||||
code="TSLA",
|
||||
name="Tesla",
|
||||
pre_close=0.0,
|
||||
open=0.0,
|
||||
high=0.0,
|
||||
low=0.0,
|
||||
close=0.0,
|
||||
vol=0,
|
||||
amount=0.0,
|
||||
turnover=0.0,
|
||||
avg=0.0,
|
||||
charts=[],
|
||||
)
|
||||
|
||||
client = object.__new__(AsyncMacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
df = await client.goods_tick_chart(74, "TSLA", query_date=20260827)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert captured[0]._ymd == 20260827
|
||||
assert len(df) == 1 and df.iloc[0]["code"] == "TSLA"
|
||||
|
||||
|
||||
async def test_async_goods_transaction_int_date() -> None:
|
||||
"""异步逐笔成交:非港股市场 int 日期转换。"""
|
||||
from easy_tdx.ex.mac_client import AsyncMacExClient
|
||||
|
||||
captured: list = []
|
||||
|
||||
async def fake_execute(cmd):
|
||||
captured.append(cmd)
|
||||
return []
|
||||
|
||||
client = object.__new__(AsyncMacExClient)
|
||||
client._execute = fake_execute # type: ignore[method-assign]
|
||||
|
||||
await client.goods_transaction(74, "TSLA", query_date=20260827, count=10)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert captured[0]._ymd == 20260827
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. CLI 回归:easy-tdx ex tick --date
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _FakeMacExClient:
|
||||
"""假客户端:记录 goods_tick_chart 收到的 query_date。"""
|
||||
|
||||
def __init__(self, received: dict) -> None:
|
||||
self._received = received
|
||||
|
||||
def goods_tick_chart(self, market: int, code: str, query_date=None) -> pd.DataFrame:
|
||||
self._received["query_date"] = query_date
|
||||
return pd.DataFrame([{"market": market, "code": code}])
|
||||
|
||||
def connect(self) -> None:
|
||||
pass
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class _FakeCtx:
|
||||
def __init__(self, received: dict) -> None:
|
||||
self._received = received
|
||||
|
||||
def __enter__(self) -> _FakeMacExClient:
|
||||
return _FakeMacExClient(self._received)
|
||||
|
||||
def __exit__(self, *args) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _patch_conn(monkeypatch: pytest.MonkeyPatch, received: dict) -> None:
|
||||
import easy_tdx.cli.conn as conn_mod
|
||||
|
||||
monkeypatch.setattr(conn_mod, "get_mac_ex_client", lambda: _FakeCtx(received))
|
||||
|
||||
|
||||
def test_cli_ex_tick_with_date(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""回归:--date 20260827 不再抛 AttributeError。
|
||||
|
||||
CLI 层透传 YYYYMMDD 整数,由 MacExClient.goods_tick_chart 内部归一为
|
||||
date(与 A 股 MacClient.get_tick_chart 模式一致);转换正确性由上面
|
||||
的客户端级测试覆盖。
|
||||
"""
|
||||
from easy_tdx.cli.cmd_ex import ex
|
||||
|
||||
received: dict = {}
|
||||
_patch_conn(monkeypatch, received)
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(ex, ["tick", "US_STOCK", "TSLA", "--date", "20260827"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert received["query_date"] == 20260827
|
||||
|
||||
|
||||
def test_cli_ex_tick_without_date(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""不带 --date 时默认 None(今天),不回归。"""
|
||||
from easy_tdx.cli.cmd_ex import ex
|
||||
|
||||
received: dict = {}
|
||||
_patch_conn(monkeypatch, received)
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(ex, ["tick", "US_STOCK", "TSLA"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert received["query_date"] is None
|
||||
Reference in New Issue
Block a user