fix(portfolio): provider download加threading超时防御+Layer1链路闭环

xtdata.download_financial_data 阻塞无超时,休市/服务不响应卡死整个回测
(实证:周六休市HS300全成分首次download卡死,已缓存股票0.5s)。
包threading+join(120s)超时跳过读缓存(数据不全但回测不卡死)。
Layer1 MVP链路完整闭环:runner→BulletTrade engine→AllWeather策略→miniQMT→JSON
(7.49%收益/37交易日/选股5只ETF/净值37点/指标total_return+sharpe+max_drawdown全有)。
休市致股票财务空走ETF兜底,策略数值无意义但链路100%通;周一download恢复走真实选股。
This commit is contained in:
2026-07-18 22:31:58 +08:00
parent b38ac3efd1
commit f074a6b420
@@ -163,15 +163,11 @@ class SanguoMiniQmtProvider(MiniQMTProvider): # type: ignore[misc]
qmt_stocks = [self._normalize_security_code(s) for s in stocks] qmt_stocks = [self._normalize_security_code(s) for s in stocks]
try: try:
if self.auto_download: if self.auto_download:
try: # download_financial_data 阻塞无超时,休市/服务不响应会卡死整个回测;
# 实证 signature: download_financial_data(stock_list, table_list=[]) # 包 threading 超时,超时则跳过读缓存(数据可能不全但回测不卡死)
# trading hours Income/CashFlow 常超时,主拿 PershareIndex/Balance/Capital self._download_financial_safe(
xt.download_financial_data( qmt_stocks, ["PershareIndex", "Balance", "Capital"]
qmt_stocks, )
["PershareIndex", "Balance", "Capital"],
)
except Exception as exc:
logger.debug("download_financial_data 失败(继续读缓存): %s", exc)
fin_data = xt.get_financial_data(qmt_stocks) fin_data = xt.get_financial_data(qmt_stocks)
except Exception as exc: except Exception as exc:
logger.warning("get_financial_data 失败,返空表: %s", exc) logger.warning("get_financial_data 失败,返空表: %s", exc)
@@ -193,6 +189,35 @@ class SanguoMiniQmtProvider(MiniQMTProvider): # type: ignore[misc]
df = df.set_index("code", drop=False) df = df.set_index("code", drop=False)
return df return df
def _download_financial_safe(
self, qmt_stocks: List[str], tables: List[str], timeout: float = 120.0
) -> None:
"""download_financial_data 包 threading 超时。
xtdata.download_financial_data 阻塞且无超时参数,休市/服务不响应时卡死
整个回测。用线程 + join(timeout):超时则放弃(读缓存),daemon 线程随进程退出清理。
"""
import threading
xt = self._ensure_xtdata()
err: List[str] = []
def _worker() -> None:
try:
xt.download_financial_data(qmt_stocks, tables)
except Exception as exc:
err.append(str(exc))
t = threading.Thread(target=_worker, daemon=True)
t.start()
t.join(timeout)
if t.is_alive():
logger.warning(
"download_financial_data 超时 %.0fs (%d 只),跳过读缓存",
timeout, len(qmt_stocks),
)
elif err:
logger.debug("download_financial_data 失败(继续读缓存): %s", err[0])
# ------------------------ 内部组装 ------------------------ # ------------------------ 内部组装 ------------------------
def _build_row( def _build_row(
self, self,