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 通过
This commit is contained in:
Simon
2026-09-09 07:29:26 +08:00
parent 9cc4bfccac
commit 93e32f4e63
7 changed files with 140 additions and 0 deletions
@@ -60,6 +60,13 @@ class FailoverProvider:
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",
@@ -225,6 +225,9 @@ class SinaProvider:
def get_stock_basic(self):
raise DataSourceNotSupported("新浪不提供股票基础信息列表")
def get_index_weight(self, index_code):
raise DataSourceNotSupported("新浪不提供指数成分接口")
def get_trade_cal(self, start, end):
raise DataSourceNotSupported("新浪不提供交易日历")
@@ -14,6 +14,7 @@ from datetime import date, datetime, timedelta
from decimal import Decimal
from typing import Any
from app.domain.entities.index import IndexWeight
from app.domain.entities.market import (
AdjustFactor,
DailyBar,
@@ -96,6 +97,28 @@ class TushareProvider:
for rec in records
]
@staticmethod
def normalize_index_weight(
records: list[dict[str, Any]], index_code_fallback: str = ""
) -> list[IndexWeight]:
"""index_weight 接口行 → IndexWeight(index_code/con_code/trade_date/weight)。"""
out: list[IndexWeight] = []
for rec in records:
code = str(rec.get("index_code") or index_code_fallback or "")
symbol = str(rec.get("con_code") or "")
if not code or not symbol:
continue
out.append(
IndexWeight(
index_code=code,
index_name=rec.get("index_name"),
trade_date=_to_date(rec.get("trade_date")) or date.min,
symbol=symbol,
weight=_to_decimal(rec.get("weight")),
)
)
return out
@staticmethod
def normalize_daily(records: list[dict[str, Any]]) -> list[DailyBar]:
bars: list[DailyBar] = []
@@ -224,6 +247,11 @@ class TushareProvider:
_RATE_LIMIT_MARKERS = ("频率超限", "每分钟", "frequenc", "too many")
def get_index_weight(self, index_code: str) -> list[IndexWeight]:
"""指数成分(Tushare index_weight 全历史,ts_code 过滤)。"""
records = self._call("index_weight", ts_code=index_code)
return self.normalize_index_weight(records, index_code_fallback=index_code)
def _call(self, api: str, **kwargs) -> list[dict[str, Any]]:
"""带限速退避的调用:频率超限按指数退避(最长 _rate_limit_wait)等待后重试。"""
last_error: Exception | None = None