Files
qlib/backend/app/infrastructure/data_sources/failover.py
T
Simon 93e32f4e63 feat(data): B1-2 指数成分同步(Provider + CLI sync index_weight)
- MarketDataProvider.get_index_weight(协议);Tushare 实现 normalize_index_weight +
  get_index_weight(ts_code=... 全历史成分权重);Sina 抛 DataSourceNotSupported;
  FailoverProvider 代理并审计每次尝试
- CLI:sync index_weight --code 000300.SH(拉取→幂等落库 index_weight→打印最新快照;
  失败走 sync_log 审计并返回非零)
- tests:Tushare 映射与调用(FakePro)、Failover 主源单源语义(新浪不支持被审计);
  全量 pytest 通过
2026-09-09 07:29:26 +08:00

191 lines
6.0 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.
"""Tushare → Sina Failover 包装(AGENT.md §7)。
规则:
- 优先 primary;primary 抛错时才尝试 fallback(避免对空结果做无谓兜底请求)
- fallback 不支持该 API(DataSourceNotSupported)或自身失败 → 抛 DataSourceError
- 每次尝试都写 SyncLog(source / 成功与否 / 行数 / 区间),禁止静默切换
"""
from __future__ import annotations
from collections.abc import Callable
from datetime import date
from typing import Any
from app.domain.entities.market import SyncLog
from app.domain.providers import MarketDataProvider
from app.infrastructure.data_sources.errors import DataSourceError, DataSourceNotSupported
class FailoverProvider:
"""以 primary 为主、fallback 为辅的 MarketDataProvider 实现。"""
name = "failover"
def __init__(
self,
primary: MarketDataProvider,
fallback: MarketDataProvider | None = None,
*,
audit: Callable[[SyncLog], None] | None = None,
) -> None:
self.primary = primary
self.fallback = fallback
self._audit = audit or (lambda _log: None)
# ---- 各 API 代理 ----
def get_stock_basic(self) -> list:
return self._with_failover(
"get_stock_basic",
primary_call=lambda: self.primary.get_stock_basic(),
fallback_call=lambda: self.fallback.get_stock_basic(),
)
def get_trade_cal(self, start: date, end: date) -> list:
return self._with_failover(
"get_trade_cal",
start=start,
end=end,
primary_call=lambda: self.primary.get_trade_cal(start, end),
fallback_call=lambda: self.fallback.get_trade_cal(start, end),
)
def get_daily(self, symbol: str, start: date, end: date) -> list:
return self._with_failover(
"get_daily",
start=start,
end=end,
primary_call=lambda: self.primary.get_daily(symbol, start, end),
fallback_call=lambda: self.fallback.get_daily(symbol, start, end),
)
def get_index_weight(self, index_code: str) -> list:
return self._with_failover(
"get_index_weight",
primary_call=lambda: self.primary.get_index_weight(index_code),
fallback_call=lambda: self.fallback.get_index_weight(index_code),
)
def get_adjust_factor(self, symbol: str, start: date, end: date) -> list:
return self._with_failover(
"get_adjust_factor",
start=start,
end=end,
primary_call=lambda: self.primary.get_adjust_factor(symbol, start, end),
fallback_call=lambda: self.fallback.get_adjust_factor(symbol, start, end),
)
def get_financial(
self, symbol: str, start: date | None = None, end: date | None = None
) -> list:
return self._with_failover(
"get_financial",
start=start,
end=end,
primary_call=lambda: self.primary.get_financial(symbol, start, end),
fallback_call=lambda: self.fallback.get_financial(symbol, start, end),
)
# ---- 内部 ----
def _with_failover(
self,
api: str,
*,
primary_call: Callable[[], list],
fallback_call: Callable[[], list] | None = None,
start: date | None = None,
end: date | None = None,
) -> list:
try:
rows = primary_call()
except Exception as exc: # noqa: BLE001 —— 统一走审计
self._log(
source=self.primary.name,
api=api,
success=False,
reason=str(exc),
start=start,
end=end,
)
return self._try_fallback(api, fallback_call, start=start, end=end, primary_error=exc)
self._log(
source=self.primary.name,
api=api,
success=True,
row_count=_len(rows),
start=start,
end=end,
)
return rows
def _try_fallback(self, api, fallback_call, *, start, end, primary_error):
if fallback_call is None or self.fallback is None:
raise DataSourceError(
f"{self.primary.name}.{api} 失败且无备用源: {primary_error}"
) from primary_error
try:
rows = fallback_call()
except DataSourceNotSupported as exc:
self._log(
source=self.fallback.name,
api=api,
success=False,
reason=f"不支持: {exc}",
start=start,
end=end,
)
raise DataSourceError(
f"{self.primary.name}.{api} 失败,备用源不支持: {primary_error}"
) from primary_error
except Exception as exc: # noqa: BLE001
self._log(
source=self.fallback.name,
api=api,
success=False,
reason=str(exc),
start=start,
end=end,
)
raise DataSourceError(
f"主备数据源均失败: primary[{self.primary.name}]={primary_error} "
f"fallback[{self.fallback.name}]={exc}"
) from exc
self._log(
source=self.fallback.name,
api=api,
success=True,
row_count=_len(rows),
start=start,
end=end,
)
return rows
def _log(
self,
*,
source: str,
api: str,
success: bool,
reason: str | None = None,
row_count: int = 0,
start: date | None = None,
end: date | None = None,
) -> None:
self._audit(
SyncLog(
source=source,
api=api,
success=success,
failure_reason=reason,
row_count=row_count,
data_start=start,
data_end=end,
)
)
def _len(rows: Any) -> int:
return len(rows) if rows is not None else 0