mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 16:54:20 +08:00
v1.17.2 引入的 QFQ 重算代码(adjust.py/client.py/test_mac_qfq_integration.py) 含 magic trailing comma 导致 ruff format 期望多行参数展开,CI 的 'ruff format --check src/ tests/' 自 v1.17.0 起一直失败。纯格式变更, 零逻辑改动(20 例 QFQ 测试全过)。
185 lines
5.9 KiB
Python
185 lines
5.9 KiB
Python
"""QFQ 本地重算的集成测试(monkeypatch,无 live server)。
|
||
|
||
验证 ``MacClient.get_stock_kline(adjust=QFQ)`` 在服务端返回负价时:
|
||
1. 触发 NONE 重抓 + XDXR 本地重算;
|
||
2. 结果全部为正、OHLC 同比缩放;
|
||
3. XDXR 取不到时降级返回原始(含负价)数据,不抛异常。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from unittest.mock import patch
|
||
|
||
import pandas as pd
|
||
|
||
from easy_tdx.mac.client import MacClient
|
||
from easy_tdx.mac.commands.symbol_bar import SymbolBarCmd
|
||
from easy_tdx.mac.enums import Adjust, Period
|
||
from easy_tdx.mac.models import MacBar
|
||
|
||
|
||
def _bar(dt: str, close: float, fq: Adjust = Adjust.NONE) -> MacBar:
|
||
"""构造单根 MacBar,OHLC 全等于 close。"""
|
||
d = datetime.fromisoformat(dt)
|
||
return MacBar(
|
||
datetime=d, open=close, high=close, low=close, close=close, vol=100.0, amount=1000.0
|
||
)
|
||
|
||
|
||
def _none_bars() -> list[MacBar]:
|
||
"""干净的 NONE 序列:除权日前 close=10,除权日 close=8(跌去 2 元分红),之后 9。"""
|
||
return [
|
||
_bar("2024-01-01", 10.0),
|
||
_bar("2024-01-02", 10.0), # cum-div
|
||
_bar("2024-01-03", 8.0), # ex-date
|
||
_bar("2024-01-04", 9.0),
|
||
]
|
||
|
||
|
||
def _qfq_broken_bars() -> list[MacBar]:
|
||
"""模拟服务端 QFQ 异常:除权日及之前返回负价。"""
|
||
return [
|
||
_bar("2024-01-01", -4.0, Adjust.QFQ),
|
||
_bar("2024-01-02", -4.0, Adjust.QFQ),
|
||
_bar("2024-01-03", 8.0, Adjust.QFQ),
|
||
_bar("2024-01-04", 9.0, Adjust.QFQ),
|
||
]
|
||
|
||
|
||
def _xdxr_df() -> pd.DataFrame:
|
||
"""单条除权除息记录:fenhong=2.0(除权日 2024-01-03)。"""
|
||
return pd.DataFrame(
|
||
[
|
||
{
|
||
"date": "2024-01-03",
|
||
"category": 1,
|
||
"fenhong": 2.0,
|
||
"peigujia": None,
|
||
"songzhuangu": None,
|
||
"peigu": None,
|
||
}
|
||
]
|
||
)
|
||
|
||
|
||
def _make_client() -> MacClient:
|
||
"""构造未连接的 MacClient(仅用于调用 _execute mock 路径)。"""
|
||
client = MacClient.__new__(MacClient)
|
||
client._xdxr_cache = {}
|
||
client._timeout = 10.0
|
||
return client
|
||
|
||
|
||
def test_qfq_negative_triggers_local_recompute():
|
||
"""服务端 QFQ 返回负价 → 用 NONE+XDXR 重算,结果全正。"""
|
||
client = _make_client()
|
||
|
||
def fake_execute(cmd: SymbolBarCmd) -> list[MacBar]:
|
||
return _qfq_broken_bars() if cmd._fq == Adjust.QFQ else _none_bars()
|
||
|
||
with (
|
||
patch.object(client, "_execute", side_effect=fake_execute),
|
||
patch("easy_tdx.client.TdxClient") as MockTdx,
|
||
):
|
||
# 让 TdxClient 上下文返回手构 XDXR
|
||
mock_inst = MockTdx.from_best_host.return_value.__enter__.return_value
|
||
mock_inst.get_xdxr_info.return_value = _xdxr_df()
|
||
|
||
df = client.get_stock_kline(
|
||
market=1,
|
||
code="601088",
|
||
period=Period.DAILY,
|
||
start=0,
|
||
count=4,
|
||
adjust=Adjust.QFQ,
|
||
)
|
||
|
||
# 重算后全部为正
|
||
assert (df["close"] > 0).all(), df["close"].tolist()
|
||
# f=(10-2)/10=0.8 → 除权日前两根 *= 0.8 = 8.0;ex-date 及之后不动
|
||
assert df["close"].tolist() == [8.0, 8.0, 8.0, 9.0]
|
||
# OHLC 同比缩放(open 也应被缩放)
|
||
assert df["open"].tolist() == [8.0, 8.0, 8.0, 9.0]
|
||
|
||
|
||
def test_qfq_clean_does_not_trigger_recompute():
|
||
"""服务端 QFQ 正常(无负价)→ 不触发重算,原样返回。"""
|
||
client = _make_client()
|
||
clean_qfq = [
|
||
_bar("2024-01-01", 8.0, Adjust.QFQ),
|
||
_bar("2024-01-02", 8.0, Adjust.QFQ),
|
||
_bar("2024-01-03", 8.0, Adjust.QFQ),
|
||
_bar("2024-01-04", 9.0, Adjust.QFQ),
|
||
]
|
||
|
||
with (
|
||
patch.object(client, "_execute", return_value=clean_qfq) as mock_exec,
|
||
patch("easy_tdx.client.TdxClient") as MockTdx,
|
||
):
|
||
df = client.get_stock_kline(
|
||
market=1,
|
||
code="601088",
|
||
period=Period.DAILY,
|
||
start=0,
|
||
count=4,
|
||
adjust=Adjust.QFQ,
|
||
)
|
||
# QFQ 干净时不应再去拉 XDXR
|
||
MockTdx.from_best_host.assert_not_called()
|
||
|
||
assert df["close"].tolist() == [8.0, 8.0, 8.0, 9.0]
|
||
# 只拉了一次(QFQ),没有第二次拉 NONE
|
||
assert mock_exec.call_count == 1
|
||
|
||
|
||
def test_qfq_recompute_xdxr_failure_degrades_gracefully():
|
||
"""XDXR 取不到 → 降级返回 NONE 数据(不再含负价),不抛异常。"""
|
||
client = _make_client()
|
||
|
||
def fake_execute(cmd: SymbolBarCmd) -> list[MacBar]:
|
||
return _qfq_broken_bars() if cmd._fq == Adjust.QFQ else _none_bars()
|
||
|
||
with (
|
||
patch.object(client, "_execute", side_effect=fake_execute),
|
||
patch("easy_tdx.client.TdxClient") as MockTdx,
|
||
):
|
||
# XDXR 抛异常 → _fetch_xdxr_records 返回 None → 降级
|
||
mock_inst = MockTdx.from_best_host.return_value.__enter__.return_value
|
||
mock_inst.get_xdxr_info.side_effect = RuntimeError("host unreachable")
|
||
|
||
df = client.get_stock_kline(
|
||
market=1,
|
||
code="601088",
|
||
period=Period.DAILY,
|
||
start=0,
|
||
count=4,
|
||
adjust=Adjust.QFQ,
|
||
)
|
||
|
||
# 降级:返回 NONE 数据(apply_forward_adjust 因 xd=None 原样返回 df)
|
||
# df 是 NONE 重抓结果(全正),但未做前复权
|
||
assert (df["close"] > 0).all()
|
||
assert df["close"].tolist() == [10.0, 10.0, 8.0, 9.0]
|
||
|
||
|
||
def test_none_adjust_skips_recompute():
|
||
"""adjust=NONE 时完全跳过 QFQ 重算逻辑。"""
|
||
client = _make_client()
|
||
with (
|
||
patch.object(client, "_execute", return_value=_none_bars()) as mock_exec,
|
||
patch("easy_tdx.client.TdxClient") as MockTdx,
|
||
):
|
||
df = client.get_stock_kline(
|
||
market=1,
|
||
code="601088",
|
||
period=Period.DAILY,
|
||
start=0,
|
||
count=4,
|
||
adjust=Adjust.NONE,
|
||
)
|
||
MockTdx.from_best_host.assert_not_called()
|
||
|
||
assert df["close"].tolist() == [10.0, 10.0, 8.0, 9.0]
|
||
assert mock_exec.call_count == 1
|