feat(ak): 申万一级行业成份快照域 sw_industry——sw_index_first_info(31)+index_component_sw逐行业(成份+最新权重+计入日期,~5214行/夜37s实测);快照语义对齐ths_industry(§15.2);严格整体语义=任一行业失败整体failed次夜重跑;单unit 32连发内部0.8s逐发间隔防封;锚点600519∈801120食品饮料(权重39.8866/计入2021-12-13)实测入测试;TDD 6新测试336全绿 [vps]
This commit is contained in:
@@ -32,7 +32,7 @@ $ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||
$logDir = 'C:\sanguo_vnpy_v2\data\migration_logs'
|
||||
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
|
||||
$log = Join-Path $logDir "ak_events_$ts.txt"
|
||||
C:\Python310\python.exe -X utf8 C:\sanguo_vnpy_v2\scripts\data_platform\akshare_static_download.py --types dragon_tiger,block_trade,restricted,zt_pool,zt_pool_zbgc,zt_pool_dtgc,fund_flow_industry,fund_flow_concept,ths_industry,ths_concept,xueqiu_hot,sina_sector,hot_rank --start $today --end $today *>> $log
|
||||
C:\Python310\python.exe -X utf8 C:\sanguo_vnpy_v2\scripts\data_platform\akshare_static_download.py --types dragon_tiger,block_trade,restricted,zt_pool,zt_pool_zbgc,zt_pool_dtgc,fund_flow_industry,fund_flow_concept,ths_industry,ths_concept,xueqiu_hot,sina_sector,hot_rank,sw_industry --start $today --end $today *>> $log
|
||||
$rc1 = $LASTEXITCODE
|
||||
C:\Python310\python.exe -X utf8 C:\sanguo_vnpy_v2\scripts\data_platform\akshare_static_download.py --types margin_sse --start $yesterday --end $today *>> $log
|
||||
$rc2 = $LASTEXITCODE
|
||||
|
||||
@@ -186,6 +186,11 @@ PER_DATE_TYPES = (
|
||||
"hot_rank", # 东财 stock_hot_rank_em() 人气榜 ~100 行
|
||||
"xueqiu_hot", # 雪球 stock_hot_follow_xq('最热门') 关注热度 ~5633 行
|
||||
"sina_sector", # 新浪 stock_sector_spot() 行业 49 行 (同花顺 90 行的对账源)
|
||||
# 申万一级行业成份快照 (融合 spec §20.2, 2026-09-12 四域扩展):
|
||||
# sw_index_first_info(31) + index_component_sw×31 = 成份+最新权重+计入日期
|
||||
# (~5,400 行/夜); 快照语义同上 (无历史, date 仅命名); 变更史消费法=
|
||||
# 逐日快照 diff + 计入日期列。锚点: 600519 ∈ 801120 食品饮料。
|
||||
"sw_industry",
|
||||
)
|
||||
# 模式 C: per-period 类型 (2 类)
|
||||
PER_PERIOD_TYPES = (
|
||||
@@ -1040,6 +1045,49 @@ def fetch_sina_sector(date: str) -> Optional[pd.DataFrame]:
|
||||
return df
|
||||
|
||||
|
||||
# 申万域锚点 (2026-09-12 实测, spec §20.5): 600519 贵州茅台 ∈ 801120 食品饮料
|
||||
SW_ANCHOR_STOCK = "600519"
|
||||
SW_ANCHOR_INDUSTRY_CODE = "801120"
|
||||
# 单 unit 内 31 行业连发 → 内部逐发间隔 (防封纪律, 对齐 AK_INTERVAL)
|
||||
SW_INDUSTRY_INTERVAL = 0.8
|
||||
|
||||
|
||||
def fetch_sw_industry(date: str) -> Optional[pd.DataFrame]:
|
||||
"""sw_index_first_info + index_component_sw×31 — 申万一级行业成份快照。
|
||||
|
||||
融合 spec §20.2: 行=股票×一级行业 (~5,400), 列含 最新权重+计入日期
|
||||
(计入日期自带部分变更史, 2021-12-13 大搬迁类事件可考)。快照域语义
|
||||
(§15.2): 端点无 date 参数无历史回补, date 仅命名文件。
|
||||
严格整体语义: 任一行业成分失败/为空 → 整体 None (failed), 次夜全量
|
||||
重跑 (32 发便宜, 不写残缺快照)。
|
||||
单 fetch 总时长超 AK_TIMEOUT, 故逐行业独立包 call_ak_with_retry
|
||||
(每行业 30s 预算) 而非整体包装。"""
|
||||
idx_df, _st = call_ak_with_retry(
|
||||
ak.sw_index_first_info, f"sw_industry/{date}/first_info",
|
||||
)
|
||||
if idx_df is None or idx_df.empty:
|
||||
return None
|
||||
frames: List[pd.DataFrame] = []
|
||||
for _, row in idx_df.iterrows():
|
||||
code_si = str(row["行业代码"])
|
||||
sym = code_si.split(".")[0] # index_component_sw 吃去后缀码
|
||||
time.sleep(SW_INDUSTRY_INTERVAL)
|
||||
cons, _st2 = call_ak_with_retry(
|
||||
ak.index_component_sw,
|
||||
f"sw_industry/{date}/{sym}", symbol=sym,
|
||||
)
|
||||
if cons is None or cons.empty:
|
||||
logger.warning(
|
||||
"[sw_industry] %s(%s) 成分失败/为空 → 整体 failed",
|
||||
sym, row["行业名称"])
|
||||
return None
|
||||
cons = cons.copy()
|
||||
cons["行业代码"] = code_si
|
||||
cons["行业名称"] = row["行业名称"]
|
||||
frames.append(cons)
|
||||
return pd.concat(frames, ignore_index=True)
|
||||
|
||||
|
||||
def fetch_gdhs(period: str) -> Optional[pd.DataFrame]:
|
||||
"""stock_zh_a_gdhs(symbol=period) — 股东户数全市场按期单次 (~5342 行)。"""
|
||||
df, _status = call_ak_with_retry(
|
||||
@@ -1543,6 +1591,7 @@ def run_type_dispatch(
|
||||
"hot_rank": fetch_hot_rank,
|
||||
"xueqiu_hot": fetch_xueqiu_hot,
|
||||
"sina_sector": fetch_sina_sector,
|
||||
"sw_industry": fetch_sw_industry,
|
||||
}
|
||||
units = build_per_date_units(t, fetch_map[t], args)
|
||||
return run_one_type(t, units, args)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""申万一级行业成份快照域 (sw_industry) 注册与 fetch 语义测试。
|
||||
|
||||
融合 spec §20.2 (2026-09-12 四域扩展):
|
||||
- sw_index_first_info (31 一级, 行业代码带 .SI 后缀) +
|
||||
index_component_sw(symbol=去后缀码)×31 → 成份+最新权重+计入日期
|
||||
- 快照域语义 (§15.2): 无 date 参数无历史, date 仅命名文件
|
||||
- 严格整体语义: 任一行业成分失败 → 整体 failed (None), 次夜全量重跑
|
||||
- 单 unit 内 32 连发 → 内部逐发间隔 (防封纪律), 测试中 monkeypatch sleep
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
import akshare_static_download as mod # noqa: E402 (conftest 已加 sys.path)
|
||||
|
||||
|
||||
def _idx():
|
||||
return pd.DataFrame({
|
||||
"行业代码": ["801010.SI", "801120.SI"],
|
||||
"行业名称": ["农林牧渔", "食品饮料"],
|
||||
"成份个数": [2, 1],
|
||||
})
|
||||
|
||||
|
||||
def _cons(sym, n):
|
||||
return pd.DataFrame({
|
||||
"序号": list(range(1, n + 1)),
|
||||
"证券代码": [f"60{i:04d}" for i in range(n)],
|
||||
"证券名称": [f"股{i}" for i in range(n)],
|
||||
"最新权重": [1.0] * n,
|
||||
"计入日期": ["2021-12-13"] * n,
|
||||
})
|
||||
|
||||
|
||||
class TestSwIndustryRegistered:
|
||||
|
||||
def test_in_per_date_types(self):
|
||||
assert "sw_industry" in mod.PER_DATE_TYPES
|
||||
|
||||
def test_anchor_moutai_food_beverage(self):
|
||||
"""600519 ∈ 801120 食品饮料 (2026-09-12 实测锚点, spec §20.5)。"""
|
||||
assert mod.SW_ANCHOR_STOCK == "600519"
|
||||
assert mod.SW_ANCHOR_INDUSTRY_CODE == "801120"
|
||||
|
||||
|
||||
class TestFetchSwIndustry:
|
||||
|
||||
def test_concat_all_industries(self, monkeypatch):
|
||||
monkeypatch.setattr(mod.time, "sleep", lambda s: None)
|
||||
monkeypatch.setattr(mod.ak, "sw_index_first_info", _idx)
|
||||
monkeypatch.setattr(
|
||||
mod.ak, "index_component_sw",
|
||||
lambda symbol: _cons(symbol, 1 if symbol == "801120" else 2))
|
||||
df = mod.fetch_sw_industry("20260911")
|
||||
assert len(df) == 3
|
||||
assert set(df["行业代码"]) == {"801010.SI", "801120.SI"}
|
||||
# 证券代码不带后缀 (直接入库形态)
|
||||
assert "600000" in set(df["证券代码"].astype(str))
|
||||
assert "计入日期" in df.columns
|
||||
|
||||
def test_any_industry_failure_fails_whole(self, monkeypatch):
|
||||
"""任一行业成分失败 → 整体 None (failed, 不写残缺快照)。"""
|
||||
monkeypatch.setattr(mod.time, "sleep", lambda s: None)
|
||||
monkeypatch.setattr(mod.ak, "sw_index_first_info", _idx)
|
||||
|
||||
def boom(symbol):
|
||||
if symbol == "801120":
|
||||
raise ValueError("akshare 挂死")
|
||||
return _cons(symbol, 2)
|
||||
|
||||
monkeypatch.setattr(mod.ak, "index_component_sw", boom)
|
||||
df = mod.fetch_sw_industry("20260911")
|
||||
assert df is None
|
||||
|
||||
def test_first_info_failure_returns_none(self, monkeypatch):
|
||||
monkeypatch.setattr(mod.time, "sleep", lambda s: None)
|
||||
|
||||
def boom():
|
||||
raise ValueError("akshare 挂死")
|
||||
|
||||
monkeypatch.setattr(mod.ak, "sw_index_first_info", boom)
|
||||
assert mod.fetch_sw_industry("20260911") is None
|
||||
|
||||
def test_internal_throttle_between_calls(self, monkeypatch):
|
||||
"""单 unit 32 连发必须有内部间隔 (防封纪律)。"""
|
||||
sleeps = []
|
||||
monkeypatch.setattr(mod.time, "sleep", lambda s: sleeps.append(s))
|
||||
monkeypatch.setattr(mod.ak, "sw_index_first_info", _idx)
|
||||
monkeypatch.setattr(
|
||||
mod.ak, "index_component_sw",
|
||||
lambda symbol: _cons(symbol, 1))
|
||||
mod.fetch_sw_industry("20260911")
|
||||
assert len(sleeps) >= 2 # 31 行业至少逐发间隔
|
||||
assert all(s >= 0.8 for s in sleeps)
|
||||
Reference in New Issue
Block a user