fix(live): 市值改本地估值委托——开盘不再依赖盘中Capital下载(策略session 08-24移交P0)——实锤:开盘miniQMT Capital表下载常超时(代码注释自曝trading hours常超时)→close×total_capital=NaN→策略sort_values无操作保持代码序,平安银行(≈3800亿)混进small_cap买入清单。修=get_fundamentals_df组装后增Step3.5:_apply_local_market_caps把market_cap/circulating_market_cap优先委托self._unified.get_fundamentals_df(fields=两列,估值parquet EOD,亿元同单位,全池同一时点口径);单股本地缺(NaN/新股)保留xt Capital已算值,unified整体异常静默回退开盘永不挂;date=None传今天(EOD取最新≤今天);_fetch_close本就读xtdata本地缓存不动。+6测试(本地优先/补NaN/空回退/异常回退/NaN不清值/date=None传today);628绿。验收=部署后首个9:30 small_cap买入清单不再0000xx连号不含超大盘 [vps]
CI/CD / test (push) Successful in 6s
CI/CD / nas-deploy (push) Successful in 38s
CI/CD / nas-verify (push) Successful in 10s

This commit is contained in:
2026-08-24 13:49:24 +08:00
parent 80fc517531
commit 40908b51f8
2 changed files with 115 additions and 0 deletions
@@ -483,6 +483,11 @@ class SanguoMiniQmtProvider(MiniQMTProvider): # type: ignore[misc]
row = self._build_row(jq_code, qmt_code, stock_fin, close)
rows.append(row)
# Step 3.5: 市值本地委托覆盖(2026-08-24 P0)。开盘 Capital 下载常超时 →
# close×total_capital=NaN → 策略 sort_values 无操作保持代码序, small_cap
# 买入清单混入超大盘(平安银行实锤)。本地估值 parquet(EOD)不依赖盘中下载。
self._apply_local_market_caps(rows, stocks, date_str)
df = pd.DataFrame(rows)
if "code" in df.columns:
df = df.set_index("code", drop=False)
@@ -491,6 +496,41 @@ class SanguoMiniQmtProvider(MiniQMTProvider): # type: ignore[misc]
df = df[keep]
return df
def _apply_local_market_caps(
self,
rows: List[Dict[str, Any]],
stocks: List[str],
date_str: Optional[str],
) -> None:
"""market_cap/circulating_market_cap 优先本地 unified 估值 parquet(亿元)。
全池同一时点口径(EOD 快照), 盘中 Capital 缓存缺/旧不再影响市值排序;
单股本地缺(新股未入昨日估值表/NaN)保留 _build_row 已算值回退 xt 路径;
unified 整体异常静默回退——开盘永不因本地读挂死。fields 按需短路只读
估值表, 大池(全市场)亦在秒级(LocalParquetProvider 批量路径实证)。
"""
try:
df = self._unified.get_fundamentals_df(
stocks,
date=(date_str or datetime.now().strftime("%Y-%m-%d")),
fields=["market_cap", "circulating_market_cap"],
)
except Exception as exc: # noqa: BLE001 - 本地失败回退 Capital 路径
logger.warning("本地市值委托失败, 回退 Capital 路径: %s", exc)
return
if df is None or df.empty or "code" not in df.columns:
return
by_code = {r["code"]: r for _, r in df.iterrows()}
for row in rows:
local = by_code.get(row.get("code"))
if local is None:
continue
for col in ("market_cap", "circulating_market_cap"):
v = local.get(col)
if v is None or pd.isna(v):
continue
row[col] = float(v)
def get_fundamentals_df_ex(
self,
stocks: List[str],
+75
View File
@@ -6,6 +6,7 @@ xtquant 没 装,通过 mock_xtquant fixture 注入 sys.modules。
from __future__ import annotations
import math
from unittest.mock import MagicMock
import pandas as pd
import pytest
@@ -90,6 +91,80 @@ class TestGetFundamentalsDf:
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})