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]
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user