fix(provider): SanguoMiniQmtProvider四缺口根治——实盘/影子momentum/small_cap共4账户8-18/8-19空转两日——①get_closes_panel(+_ex)委托本地dbbardata(与回测LocalUnifiedProvider同源同口径;base与xtdata SDK均无此方法,bullet_trade回退链终断AttributeError,momentum牛熊/small_cap动量瘫)②get_fundamentals_df补fields契约(keep=code+请求列,对齐unified;原small_cap fields=[market_cap,eps]直接TypeError)+_ex别名③get_constituent_ex(+get_index_stocks覆盖)委托constituent_unified并集(历史日期口径,治前视;表缺指数WARNING回退base最新成份,宁可降级不空转=本次事故教训)④config的db_path/data_dir透传统一provider,VPS缺省走生产库;+10测试(复刻VPS AttributeError实锤+回退路径);962绿;VPS实库冒烟panel新鲜到08-18/000985=5128只(前后端session巡检实锤清单全闭环) [vps]
CI/CD / test (push) Successful in 13s
CI/CD / nas-deploy (push) Successful in 32s
CI/CD / nas-verify (push) Successful in 14s

This commit is contained in:
2026-08-19 10:59:48 +08:00
parent e522cab9a4
commit 78d35baae5
2 changed files with 218 additions and 3 deletions
+131
View File
@@ -141,3 +141,134 @@ class TestSetDataProviderInjection:
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), # 被踢也在并集
],
)
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"]