Files
sanguo_vnpy_v2/tests/portfolio/test_provider.py
T

603 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""SanguoMiniQmtProvider 单元测试(mock xtquant)。
bullet-trade 装了(0.2.0+),MiniQMTProvider 基类可继承。
xtquant 没 装,通过 mock_xtquant fixture 注入 sys.modules。
"""
from __future__ import annotations
import math
from unittest.mock import MagicMock
import pandas as pd
import pytest
from sanguo_portfolio import SanguoMiniQmtProvider
pytestmark = pytest.mark.requires_bullet_trade
class TestSanguoMiniQmtProviderInstantiation:
def test_can_instantiate_with_mock_xtquant(self, mock_xtquant):
# Arrange + Act
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
# Assert
assert provider.name == "sanguo_miniqmt"
# 应继承 MiniQMTProvider
from bullet_trade.data.providers.miniqmt import MiniQMTProvider
assert isinstance(provider, MiniQMTProvider)
class TestGetFundamentalsDf:
def test_returns_dataframe_with_required_columns(self, mock_xtquant):
# Arrange
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
stocks = ["600519.XSHG", "601318.XSHG"]
# Act
df = provider.get_fundamentals_df(stocks, date="2024-09-30")
# Assert
assert isinstance(df, pd.DataFrame)
assert len(df) == 2
# 核心列都在
for col in [
"code", "market_cap", "circulating_market_cap",
"pe_ratio", "pb_ratio", "ps_ratio", "pcf_ratio",
"roe", "roa", "eps",
"total_liability", "total_sheet_owner_equities", "retained_profit",
"roic",
]:
assert col in df.columns, f"missing col: {col}"
# index 是 jq-style code
assert "600519.XSHG" in df.index
def test_empty_stocks_returns_empty_df(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df([], date="2024-09-30")
assert isinstance(df, pd.DataFrame)
assert len(df) == 0
# 空表也要有列定义,方便上层 select
assert "code" in df.columns
def test_market_cap_in_yi_unit(self, mock_xtquant):
"""close × total_capital / 1e8 = 亿元。茅台 1600 × 12.56e8 / 1e8 = 20096 亿。"""
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
mc = float(df.iloc[0]["market_cap"])
# 茅台市值应在 20000 亿左右(允许 close 1600±10)
assert 19000 < mc < 22000, f"market_cap 异常: {mc}"
def test_pe_ratio_finite_for_profitable_stock(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
pe = float(df.iloc[0]["pe_ratio"])
assert math.isfinite(pe)
assert pe > 0
def test_roic_computed(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
roic = float(df.iloc[0]["roic"])
# 茅台 mock 数据:oper=1.2e10, tax=25%, eqy=2.2e11, debt=0, cash=1.7e11
# NOPAT = 1.2e10 * 0.75 = 9e9
# IC = 2.2e11 + 0 - 1.7e11 = 5e10
# ROIC = 9e9 / 5e10 = 0.18
assert 0.05 < roic < 0.5, f"ROIC 异常: {roic}"
def test_financial_data_failure_returns_empty_df(self, mock_xtquant):
"""xtdata.get_financial_data 抛异常时返空表(不崩)。"""
mock_xtquant["xtdata"].get_financial_data.side_effect = Exception("QMT offline")
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
assert df.empty
class TestFundamentalsMarketCapLocalization:
"""2026-08-24 P0: 市值改本地估值委托, 开盘不再依赖盘中 Capital 下载。
实锤: 开盘 Capital 下载常超时 → close×total_capital=NaN → 策略 sort_values
无操作保持代码序, 平安银行(≈3800亿)混进 small_cap 买入清单。修 = market_cap/
circulating_market_cap 优先本地 unified 估值 parquet(EOD, 亿元同单位, 全池
同一时点口径); 本地缺/异常静默回退 xt Capital 路径。"""
@staticmethod
def _local_unified(rows=None, side_effect=None):
m = MagicMock()
if side_effect is not None:
m.get_fundamentals_df.side_effect = side_effect
else:
m.get_fundamentals_df.return_value = pd.DataFrame(rows or [])
return m
def test_local_market_cap_wins_over_capital_path(self, mock_xtquant):
"""本地有值 → 覆盖 close×Capital(全池同口径优先, 非「仅补 NaN」)。"""
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
provider._unified = self._local_unified([
{"code": "600519.XSHG", "market_cap": 3500.0,
"circulating_market_cap": 3400.0},
])
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
assert abs(float(df.iloc[0]["market_cap"]) - 3500.0) < 1e-6
assert abs(float(df.iloc[0]["circulating_market_cap"]) - 3400.0) < 1e-6
def test_local_fills_nan_when_capital_missing(self, mock_xtquant):
"""Capital 缺(盘中下载超时形态) → 本地补上, 不再 NaN。"""
mock_xtquant["xtdata"].get_financial_data.return_value = {} # 无任何表
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
provider._unified = self._local_unified([
{"code": "600519.XSHG", "market_cap": 21000.0,
"circulating_market_cap": 21000.0},
])
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
assert abs(float(df.iloc[0]["market_cap"]) - 21000.0) < 1e-6
def test_fallback_to_capital_when_local_empty(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
provider._unified = self._local_unified([])
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
mc = float(df.iloc[0]["market_cap"])
assert 19000 < mc < 22000 # close×Capital 原路径(≈20096 亿)
def test_fallback_when_local_raises(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
provider._unified = self._local_unified(side_effect=RuntimeError("parquet io"))
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
assert 19000 < float(df.iloc[0]["market_cap"]) < 22000
def test_local_nan_keeps_capital_value(self, mock_xtquant):
"""本地 NaN(新股未入估值表)不清掉可算值; 同行其它列正常覆盖。"""
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
provider._unified = self._local_unified([
{"code": "600519.XSHG", "market_cap": float("nan"),
"circulating_market_cap": 999.0},
])
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
assert 19000 < float(df.iloc[0]["market_cap"]) < 22000 # NaN 不覆盖
assert abs(float(df.iloc[0]["circulating_market_cap"]) - 999.0) < 1e-6
def test_none_date_passes_today_to_unified(self, mock_xtquant):
"""live date=None(9:30 选股) → 本地委托收到今天(EOD 估值取最新≤今天)。"""
from datetime import datetime as _dt
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
provider._unified = self._local_unified([])
provider.get_fundamentals_df(["600519.XSHG"], date=None)
kwargs = provider._unified.get_fundamentals_df.call_args.kwargs
assert kwargs["date"] == _dt.now().strftime("%Y-%m-%d")
assert kwargs["fields"] == ["market_cap", "circulating_market_cap"]
class TestGetFundamentalsQueryDictMode:
def test_dict_with_stocks_returns_dataframe(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
query = {"stocks": ["600519.XSHG", "601318.XSHG"], "date": "2024-09-30"}
df = provider.get_fundamentals(query)
assert len(df) == 2
def test_dict_with_filter_callable_applied(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
query = {
"stocks": ["600519.XSHG", "601318.XSHG"],
"date": "2024-09-30",
"filter": lambda d: d["roe"] > 0.3, # 只保留茅台(归一后 roe=0.30)
}
df = provider.get_fundamentals(query)
assert len(df) == 1
assert df.iloc[0]["code"] == "600519.XSHG"
def test_dict_with_order_by_applied(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
query = {
"stocks": ["600519.XSHG", "601318.XSHG"],
"date": "2024-09-30",
"order_by": [("market_cap", "desc")],
}
df = provider.get_fundamentals(query)
assert df.iloc[0]["code"] == "600519.XSHG" # 茅台市值 > 平安
def test_dict_with_limit_applied(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
query = {
"stocks": ["600519.XSHG", "601318.XSHG"],
"date": "2024-09-30",
"limit": 1,
}
df = provider.get_fundamentals(query)
assert len(df) == 1
class TestSetDataProviderInjection:
def test_set_data_provider_accepts_sanguo_provider(self, mock_xtquant):
"""set_data_provider 注入 SanguoMiniQmtProvider 实例。"""
from bullet_trade.data.api import get_data_provider, set_data_provider
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
# Act
set_data_provider(provider)
# Assert
active = get_data_provider()
assert active is provider
assert active.name == "sanguo_miniqmt"
# ======================== 本地库委托(2026-08-19 生产缺口修复) ========================
# VPS 实盘/影子 8-18/8-19 连续两天空转根因:SanguoMiniQmtProvider 缺
# get_closes_panel/get_constituent_ex + get_fundamentals_df 不认 fields +
# get_index_stocks 忽略历史日期(前后端 session 巡检实锤,见 memory
# data-session-todo-miniqmt-provider-gaps)。修法:内部持有 LocalUnifiedProvider
# 读本地 dbbardata/constituent_unified(与回测同口径)。
def _make_delegate_db(tmp_path) -> str:
"""dbbardata + constituent_unified 小样本库(委托 LocalUnifiedProvider 用)。"""
import sqlite3
db = tmp_path / "delegate.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE dbbardata(symbol TEXT, exchange TEXT, datetime TEXT, "
"interval TEXT, volume REAL, turnover REAL, open_interest REAL, "
"open_price REAL, high_price REAL, low_price REAL, close_price REAL)"
)
for i, dt in enumerate(["2024-06-18 00:00:00", "2024-06-19 00:00:00",
"2024-06-20 00:00:00"]):
c.execute(
"INSERT INTO dbbardata VALUES(?,?,?,?,?,?,?,?,?,?,?)",
("600519", "SSE", dt, "d", 1000.0, 1e6, 0,
1000.0 + i, 1005.0 + i, 995.0 + i, 1000.0 + i),
)
c.execute(
"CREATE TABLE constituent_unified(index_code TEXT, code TEXT, "
"code_name TEXT, source TEXT, in_current INT, was_removed INT)"
)
c.executemany(
"INSERT INTO constituent_unified VALUES(?,?,?,?,?,?)",
[
("000985", "600519", "贵州茅台", "csindex", 1, 0),
("000985", "000858", "五粮液", "csindex", 0, 1), # 被踢也在并集
],
)
# 涨跌停/停牌样本(2026-08-22): 000858 06-18 close=10 → 06-19 close=11
# = 主板 +10% 涨停; 600000 06-19 volume=0 = 停牌日(baostock 停牌 bar 有行无量)
for i, close in enumerate([10.0, 11.0]):
c.execute(
"INSERT INTO dbbardata VALUES(?,?,?,?,?,?,?,?,?,?,?)",
("000858", "SZSE", f"2024-06-1{8 + i} 00:00:00", "d",
2000.0, 1e6, 0, close, close + 0.2, close - 0.2, close),
)
for dt, vol in [("2024-06-18 00:00:00", 5000.0), ("2024-06-19 00:00:00", 0.0)]:
c.execute(
"INSERT INTO dbbardata VALUES(?,?,?,?,?,?,?,?,?,?,?)",
("600000", "SSE", dt, "d", vol, vol * 1600.0, 0,
8.0, 8.05, 7.95, 8.0),
)
c.commit()
c.close()
return str(db)
@pytest.fixture
def delegate_provider(mock_xtquant, tmp_path):
"""配好本地库路径的 SanguoMiniQmtProvider(db_path/data_dir 透传统一 provider)。"""
db = _make_delegate_db(tmp_path)
return SanguoMiniQmtProvider({
"db_path": db, "data_dir": str(tmp_path), "auto_download": False,
})
class TestGetClosesPanelDelegation:
"""get_closes_panel/get_closes_panel_ex 委托本地 dbbardata(同回测口径)。"""
def test_returns_wide_table_from_local_db(self, delegate_provider):
panel = delegate_provider.get_closes_panel(
["600519.XSHG"], "2024-06-18", "2024-06-20", fq="raw",
)
assert isinstance(panel, pd.DataFrame)
assert list(panel.columns) == ["600519.XSHG"]
assert len(panel) == 3
assert abs(panel["600519.XSHG"].iloc[0] - 1000.0) < 1e-6
assert abs(panel["600519.XSHG"].iloc[-1] - 1002.0) < 1e-6
def test_ex_alias_returns_same_result(self, delegate_provider):
old = delegate_provider.get_closes_panel(
["600519.XSHG"], "2024-06-18", "2024-06-20", fq="raw",
)
ex = delegate_provider.get_closes_panel_ex(
["600519.XSHG"], "2024-06-18", "2024-06-20", fq="raw",
)
pd.testing.assert_frame_equal(old, ex)
def test_missing_symbol_returns_nan_column(self, delegate_provider):
panel = delegate_provider.get_closes_panel(
["600519.XSHG", "999999.XSHG"], "2024-06-18", "2024-06-20",
)
assert list(panel.columns) == ["600519.XSHG", "999999.XSHG"]
assert panel["999999.XSHG"].isna().all()
def test_pure_digit_codes_accepted(self, delegate_provider):
panel = delegate_provider.get_closes_panel(
["600519"], "2024-06-18", "2024-06-20",
)
assert abs(panel["600519"].iloc[-1] - 1002.0) < 1e-6
class TestGetIndexStocksDelegation:
"""get_index_stocks/get_constituent_ex 委托 constituent_unified(支持历史日期口径)。"""
def test_reads_constituent_unified_union(self, delegate_provider):
stocks = delegate_provider.get_index_stocks("000985.XSHG", "2024-06-19")
# 并集语义:在册 + 被踢(was_removed)都返回
assert set(stocks) == {"600519.XSHG", "000858.XSHE"}
def test_constituent_ex_delegates_same(self, delegate_provider):
old = delegate_provider.get_index_stocks("000985.XSHG", "2024-06-19")
ex = delegate_provider.get_constituent_ex("000985.XSHG", "2024-06-19")
assert old == ex
def test_unknown_index_falls_back_to_xt_latest(self, delegate_provider, mock_xtquant, caplog):
"""表里没有的指数 → WARNING + 回退 miniQMT 最新成分(宁可降级不空转)。"""
mock_xtquant["xtdata"].get_index_weight.return_value = {"600519.SH": 0.5}
with caplog.at_level("WARNING", logger="sanguo_portfolio.providers.sanguo_fundamentals"):
stocks = delegate_provider.get_index_stocks("399303.XSHE", "2024-06-19")
assert stocks == ["600519.XSHG"]
assert any("回退" in r.message for r in caplog.records)
class TestGetFundamentalsDfFields:
"""get_fundamentals_df 加 fields 契约(对齐 unified:keep = code + 请求列)。"""
def test_fields_filters_columns(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(
["600519.XSHG"], date="2024-09-30", fields=["market_cap", "eps"],
)
assert list(df.columns) == ["code", "market_cap", "eps"]
assert 19000 < float(df.iloc[0]["market_cap"]) < 22000
def test_fields_none_keeps_all_columns(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
assert "roe" in df.columns # fields=None 全列(向后兼容)
def test_ex_alias_accepts_fields(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df_ex(
["600519.XSHG"], date="2024-09-30", fields=["eps"],
)
assert list(df.columns) == ["code", "eps"]
# ======================== 涨跌停/停牌批量(2026-08-22,策略session巡检实锤) ========================
# SanguoMiniQmtProvider 缺 get_limit_status_batch → 实盘策略 _get_limit_status
# getattr(...,None) 静默返 {} → filter_limitup/limitdown/paused 全走"无数据"分支
# = 实盘涨跌停/停牌过滤完全失效(涨停股照买/停牌股照挂单)。修法:历史日期委托
# unified(同回测口径) + 当日盘中单次批量 get_full_tick(线程超时)。
# ⚠️ 失败一律返"全清白 map"(每 code {False,False,False})而非 {}:filter_paused
# 对 map 内缺失 code 是剔除语义,{} 会让它全剔除(空转事故形态)。
from datetime import datetime as _dt
_CLEAR = {"is_limit_up": False, "is_limit_down": False, "is_paused": False}
def _today() -> str:
return _dt.now().strftime("%Y-%m-%d")
class TestGetLimitStatusBatchHistorical:
"""历史日期(<今日,实盘主路径=策略传 previous_date)委托本地 dbbardata。"""
def test_method_exists(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
assert callable(getattr(provider, "get_limit_status_batch", None))
def test_normal_stock_all_false(self, delegate_provider):
out = delegate_provider.get_limit_status_batch(["600519.XSHG"], "2024-06-19")
assert out["600519.XSHG"] == dict(_CLEAR)
def test_ten_pct_limit_up_detected(self, delegate_provider):
# fixture: 000858 06-18 close=10.0 → 06-19 close=11.0 = 主板涨停
out = delegate_provider.get_limit_status_batch(["000858.XSHE"], "2024-06-19")
assert out["000858.XSHE"]["is_limit_up"] is True
assert out["000858.XSHE"]["is_paused"] is False
def test_zero_volume_marks_paused(self, delegate_provider):
# fixture: 600000 06-19 volume=0(停牌日 bar 存在)
out = delegate_provider.get_limit_status_batch(["600000.XSHG"], "2024-06-19")
assert out["600000.XSHG"]["is_paused"] is True
def test_no_bar_returns_none_entry(self, delegate_provider):
out = delegate_provider.get_limit_status_batch(["999999.XSHG"], "2024-06-19")
assert out["999999.XSHG"] is None # unified 语义:无 bar=None(filter_paused 剔除)
def test_delegation_failure_returns_all_clear(self, delegate_provider, monkeypatch, caplog):
"""unified 抛异常 → 全清白(不是 {}、不是 None):失败不产生误伤/空转。"""
def _boom(codes, date):
raise RuntimeError("db locked")
monkeypatch.setattr(
delegate_provider._unified, "get_limit_status_batch", _boom)
with caplog.at_level(
"WARNING", logger="sanguo_portfolio.providers.sanguo_fundamentals"):
out = delegate_provider.get_limit_status_batch(
["600519.XSHG", "000858.XSHE"], "2024-06-19")
assert out["600519.XSHG"]["is_paused"] is False
assert out["000858.XSHE"]["is_limit_up"] is False
assert any("全清白" in r.message for r in caplog.records)
def test_empty_codes_returns_empty_dict(self, delegate_provider):
assert delegate_provider.get_limit_status_batch([], "2024-06-19") == {}
def test_historical_path_never_calls_full_tick(self, delegate_provider, mock_xtquant):
delegate_provider.get_limit_status_batch(["600519.XSHG"], "2024-06-19")
mock_xtquant["xtdata"].get_full_tick.assert_not_called()
class TestGetLimitStatusBatchRealtime:
"""当日盘中(date=None 或=今日):单次批量 get_full_tick + lastClose 算涨跌停。"""
def test_today_limit_up_from_tick(self, delegate_provider, mock_xtquant):
mock_xtquant["xtdata"].get_full_tick.return_value = {
"600519.SH": {"lastPrice": 11.0, "lastClose": 10.0, "volume": 12345},
}
out = delegate_provider.get_limit_status_batch(["600519.XSHG"])
assert out["600519.XSHG"]["is_limit_up"] is True
assert out["600519.XSHG"]["is_paused"] is False
mock_xtquant["xtdata"].get_full_tick.assert_called_once()
def test_today_date_string_routes_realtime(self, delegate_provider, mock_xtquant):
mock_xtquant["xtdata"].get_full_tick.return_value = {
"600519.SH": {"lastPrice": 10.5, "lastClose": 10.0, "volume": 100},
}
out = delegate_provider.get_limit_status_batch(["600519.XSHG"], _today())
assert out["600519.XSHG"]["is_limit_up"] is False
def test_chinext_uses_twenty_pct_band(self, delegate_provider, mock_xtquant):
# 创业板 20%: last=11.0/prev=10.0 → 未到 12.0 上限 = 未涨停
# (若误用主板 10% 口径会误判涨停——本测试钉死板块感知)
mock_xtquant["xtdata"].get_full_tick.return_value = {
"300001.SZ": {"lastPrice": 11.0, "lastClose": 10.0, "volume": 100},
}
out = delegate_provider.get_limit_status_batch(["300001.XSHE"])
assert out["300001.XSHE"]["is_limit_up"] is False
def test_limit_down_from_tick(self, delegate_provider, mock_xtquant):
mock_xtquant["xtdata"].get_full_tick.return_value = {
"600519.SH": {"lastPrice": 9.0, "lastClose": 10.0, "volume": 100},
}
out = delegate_provider.get_limit_status_batch(["600519.XSHG"])
assert out["600519.XSHG"]["is_limit_down"] is True
def test_zero_volume_with_price_marks_paused(self, delegate_provider, mock_xtquant):
mock_xtquant["xtdata"].get_full_tick.return_value = {
"600519.SH": {"lastPrice": 10.0, "lastClose": 10.0, "volume": 0},
}
out = delegate_provider.get_limit_status_batch(["600519.XSHG"])
assert out["600519.XSHG"]["is_paused"] is True
def test_missing_tick_all_clear_not_none(self, delegate_provider, mock_xtquant):
"""tick 缺该 code → 全清白(绝不能 None:filter_paused 会剔除)。"""
mock_xtquant["xtdata"].get_full_tick.return_value = {}
out = delegate_provider.get_limit_status_batch(["600519.XSHG"])
assert out["600519.XSHG"] == dict(_CLEAR)
def test_invalid_price_all_clear(self, delegate_provider, mock_xtquant):
mock_xtquant["xtdata"].get_full_tick.return_value = {
"600519.SH": {"lastPrice": 0.0, "lastClose": 10.0, "volume": 0},
}
out = delegate_provider.get_limit_status_batch(["600519.XSHG"])
assert out["600519.XSHG"] == dict(_CLEAR)
def test_tick_exception_returns_all_clear(self, delegate_provider, mock_xtquant):
mock_xtquant["xtdata"].get_full_tick.side_effect = RuntimeError("qmt busy")
out = delegate_provider.get_limit_status_batch(["600519.XSHG", "000858.XSHE"])
assert out["600519.XSHG"] == dict(_CLEAR)
assert out["000858.XSHE"] == dict(_CLEAR)
def test_timeout_returns_all_clear(self, delegate_provider, mock_xtquant):
"""get_full_tick 挂死 → 线程超时(2197fea 同款教训)全清白,不卡策略。"""
import time as _time
def _slow(codes):
_time.sleep(1.0)
return {}
mock_xtquant["xtdata"].get_full_tick.side_effect = _slow
out = delegate_provider._limit_status_realtime(["600519.XSHG"], timeout=0.2)
assert out["600519.XSHG"] == dict(_CLEAR)
class TestGetValueMetricsDelegation:
"""get_value_metrics(+batch) 委托本地三表 parquet(与回测同口径)。
2026-08-22 方法面对照扫出的同款「回测有实盘无」洞:value_selection(+_ex)
getattr 探测;该策略暂未入实盘舰队=提前堵(下次上舰不会再踩)。
"""
def test_delegates_to_unified(self, delegate_provider, monkeypatch):
def _fake(stock, date):
return {"roe": 0.3}
monkeypatch.setattr(
delegate_provider._unified, "get_value_metrics", _fake)
out = delegate_provider.get_value_metrics("600519.XSHG", "2024-06-19")
assert out == {"roe": 0.3}
def test_failure_returns_none(self, delegate_provider, monkeypatch):
def _boom(stock, date):
raise RuntimeError("parquet missing")
monkeypatch.setattr(
delegate_provider._unified, "get_value_metrics", _boom)
assert delegate_provider.get_value_metrics(
"600519.XSHG", "2024-06-19") is None
def test_batch_delegates(self, delegate_provider, monkeypatch):
def _fake(stocks, date):
return {s: {"roe": 0.3} for s in stocks}
monkeypatch.setattr(
delegate_provider._unified, "get_value_metrics_batch", _fake)
out = delegate_provider.get_value_metrics_batch(
["600519.XSHG", "000858.XSHE"], "2024-06-19")
assert out["000858.XSHE"] == {"roe": 0.3}
def test_batch_failure_returns_empty(self, delegate_provider, monkeypatch):
def _boom(stocks, date):
raise RuntimeError("parquet missing")
monkeypatch.setattr(
delegate_provider._unified, "get_value_metrics_batch", _boom)
assert delegate_provider.get_value_metrics_batch(
["600519.XSHG"], "2024-06-19") == {}
class TestFullMethodSurfaceAlignment:
"""裸名兜齐(2026-08-22 用户拍板:不论当前有无消费方,统一堵光同款洞)。
get_constituent = get_index_stocks 纯别名(unified spec §6 语义);
get_price_ex 转发 base get_price(签名/默认值对齐 unified,fq 默认 'raw')。
"""
def test_get_constituent_alias_equals_index_stocks(self, delegate_provider):
old = delegate_provider.get_index_stocks("000985.XSHG", "2024-06-19")
via = delegate_provider.get_constituent("000985.XSHG", "2024-06-19")
assert via == old
assert set(via) == {"600519.XSHG", "000858.XSHE"}
def test_get_price_ex_forwards_to_base_get_price(self, delegate_provider, monkeypatch):
from bullet_trade.data.providers.miniqmt import MiniQMTProvider
seen = {}
def _rec(self, security, **kw):
seen["security"] = security
seen.update(kw)
return "FAKE_DF"
monkeypatch.setattr(MiniQMTProvider, "get_price", _rec)
out = delegate_provider.get_price_ex(
"600519.XSHG", start_date="2024-06-18", end_date="2024-06-19")
assert out == "FAKE_DF"
assert seen["security"] == "600519.XSHG"
assert seen["start_date"] == "2024-06-18"
assert seen["end_date"] == "2024-06-19"
assert seen["fq"] == "raw" # 对齐 unified 默认(base 裸 get_price 是 'pre')
def test_get_price_ex_passthrough_kwargs(self, delegate_provider, monkeypatch):
from bullet_trade.data.providers.miniqmt import MiniQMTProvider
seen = {}
def _rec(self, security, **kw):
seen.update(kw)
return None
monkeypatch.setattr(MiniQMTProvider, "get_price", _rec)
delegate_provider.get_price_ex(
["600519.XSHG"], frequency="1m", fq="pre", count=5, panel=False)
assert seen["frequency"] == "1m"
assert seen["fq"] == "pre"
assert seen["count"] == 5
assert seen["panel"] is False
# 方法面钉死测试已迁至 test_provider_method_surface.py(单一真相,
# fail-not-skip 牙齿)——本模块的 requires_bullet_trade mark 会让它在
# bullet_trade 缺失时被 conftest 自动 skip(钉死虚标,issue #35),故移出。