fix: 板块成分股/排行报价分页合并顺序颠倒 — 多页前插改按页序追加

BoardMembersQuotesCmd 分页合并曾用 batch + all_quotes 前插,而协议 start=0
返回排序后最前一页:成分股超过单页 80 只的板块(半导体 SH881319、
光学光电 SH881329、通信设备 SH881338 等)弹窗涨跌幅榜从第 81~100 名开头,
前 80 名被压到后面,肉眼即乱序;≤80 只的板块单页不受影响。

同步/异步 get_board_members 与 get_stock_quotes_list 共 4 处统一改为
按页序追加(all_quotes = all_quotes + batch),回归测试钉死多页合并顺序、
start 偏移推进与末页不足一页的终止行为。

注:test_ex_reconnect::test_all_retries_relogin_each_time 存量失败
(干净工作树同样失败),与本修复无关,另案处理。
This commit is contained in:
Justin Gu
2026-09-05 03:30:52 +08:00
parent 2b86a7d588
commit 7456f81c80
2 changed files with 107 additions and 4 deletions
+4 -4
View File
@@ -388,7 +388,7 @@ class MacClient:
)
if not batch:
break
all_quotes = batch + all_quotes
all_quotes = all_quotes + batch
fetched += len(batch)
offset += len(batch)
if len(batch) < page_size:
@@ -799,7 +799,7 @@ class MacClient:
)
if not batch:
break
all_quotes = batch + all_quotes
all_quotes = all_quotes + batch
fetched += len(batch)
offset += len(batch)
if len(batch) < page_size:
@@ -1448,7 +1448,7 @@ class AsyncMacClient(AsyncHeartbeatMixin):
)
if not batch:
break
all_quotes = batch + all_quotes
all_quotes = all_quotes + batch
fetched += len(batch)
offset += len(batch)
if len(batch) < page_size:
@@ -1787,7 +1787,7 @@ class AsyncMacClient(AsyncHeartbeatMixin):
)
if not batch:
break
all_quotes = batch + all_quotes
all_quotes = all_quotes + batch
fetched += len(batch)
offset += len(batch)
if len(batch) < page_size:
+103
View File
@@ -0,0 +1,103 @@
"""成分股/排行报价分页合并顺序回归测试。
真实 bugv1.32.x):BoardMembersQuotesCmd 分页合并曾用 ``batch + all_quotes``
前插,而协议 ``start=0`` 返回排序后**最前**的一页——多页时整表被按页倒序拼接:
成分股超过单页 80 只的板块(如半导体 SH881319),弹窗涨跌幅榜从第 81 名开头,
前 80 名被压到后面,肉眼即"乱序"
本测试钉死:多页合并必须按请求顺序追加,且 start 偏移正确推进。
"""
from unittest.mock import patch
import pytest
from easy_tdx.mac.client import AsyncMacClient, MacClient
from easy_tdx.mac.enums import SortOrder, SortType
from easy_tdx.mac.models import MacQuoteField
_TOTAL = 100 # 超过单页 80,触发两页
def _make_rows(start: int, n: int) -> list[MacQuoteField]:
"""模拟服务器:按涨跌幅降序返回第 start..start+n 名。"""
def code(i: int) -> str:
return f"{600000 + i:06d}"
return [
MacQuoteField(
market=1,
code=code(i),
name=f"{i}",
fields={"change_pct": 10.0 - i * 0.2},
)
for i in range(start, start + n)
]
def _fake_execute_factory(seen: list):
def fake_execute(cmd):
seen.append(cmd)
# 真实服务器按剩余行数返回(末页不足 page_size),模拟之
n = min(cmd._page_size, _TOTAL - cmd._start)
return _make_rows(cmd._start, n)
return fake_execute
def test_board_members_pages_appended_in_order():
"""get_board_members 多页合并按页序追加:第 0 行=第 1 名,第 80 行=第 81 名。"""
client = MacClient.__new__(MacClient)
seen: list = []
with patch.object(client, "_execute", side_effect=_fake_execute_factory(seen)):
df = client.get_board_members(
"881319",
count=_TOTAL,
sort_type=SortType.CHANGE_PCT,
sort_order=SortOrder.DESC,
)
assert len(df) == _TOTAL
assert df["code"].tolist() == [f"{600000 + i:06d}" for i in range(_TOTAL)]
# 两页:start 080 行)→ start 8020 行)
assert [c._start for c in seen] == [0, 80]
@pytest.mark.asyncio
async def test_board_members_async_pages_appended_in_order():
client = AsyncMacClient.__new__(AsyncMacClient)
seen: list = []
async def fake_execute(cmd):
seen.append(cmd)
return _make_rows(cmd._start, cmd._page_size)
with patch.object(client, "_execute", side_effect=fake_execute):
df = await client.get_board_members(
"881319",
count=_TOTAL,
sort_type=SortType.CHANGE_PCT,
sort_order=SortOrder.DESC,
)
assert df["code"].tolist() == [f"{600000 + i:06d}" for i in range(_TOTAL)]
assert [c._start for c in seen] == [0, 80]
def test_quotes_list_pages_appended_in_order():
"""get_stock_quotes_list(看板涨跌榜数据源)同样按页序追加。"""
client = MacClient.__new__(MacClient)
seen: list = []
with patch.object(client, "_execute", side_effect=_fake_execute_factory(seen)):
df = client.get_stock_quotes_list(
category=1, # Category.A
count=_TOTAL,
sort_type=SortType.CHANGE_PCT,
sort_order=SortOrder.DESC,
)
assert df["code"].tolist() == [f"{600000 + i:06d}" for i in range(_TOTAL)]
assert [c._start for c in seen] == [0, 80]