From db789d6e9c9b55f09cc9d35f60af70c2c174d140 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Wed, 2 Sep 2026 08:07:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(data):=20akshare=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E7=B1=BB=E6=89=A9=E6=B6=A8=E5=81=9C=E6=B1=A0=E4=B8=89=E4=BB=B6?= =?UTF-8?q?=E5=A5=97=E2=80=94=E2=80=94daily=5Fstock=5Fanalysis=E8=B0=83?= =?UTF-8?q?=E7=A0=94=E8=90=BD=E5=9C=B0=E7=AC=AC=E4=B8=80=E6=AD=A5;?= =?UTF-8?q?=E5=8F=8C=E6=9C=BA=E5=AE=9E=E6=B5=8B(Mac+VPS=E7=94=9F=E4=BA=A7I?= =?UTF-8?q?P)=E5=88=86=E6=B5=81:push2ex=E9=9B=86=E7=BE=A4(zt=5Fpool/zbgc/d?= =?UTF-8?q?tgc)=E8=BF=9E=E6=89=93=E5=A4=9A=E8=BD=AE=E5=85=A8=E7=A8=B3?= =?UTF-8?q?=E5=8F=AF=E7=94=A8,push2=20clist=E9=9B=86=E7=BE=A4(=E8=A1=8C?= =?UTF-8?q?=E4=B8=9A/=E6=A6=82=E5=BF=B5=E6=A6=9C/=E6=9D=BF=E5=9D=97?= =?UTF-8?q?=E8=B5=84=E9=87=91=E6=B5=81/=E4=B8=BB=E5=8A=9B=E8=B5=84?= =?UTF-8?q?=E9=87=91)=E4=B8=8Epush2his(cyq=E7=AD=B9=E7=A0=81/=E4=B8=AA?= =?UTF-8?q?=E8=82=A1=E8=B5=84=E9=87=91=E6=B5=81)=E6=AF=8FIP=E9=95=BF?= =?UTF-8?q?=E7=AA=97=E6=9A=97=E9=85=8D=E9=A2=9D(=E9=A6=96=E5=8F=91200?= =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E5=8F=91=E8=B5=B7=E5=85=A8reset,30s/?= =?UTF-8?q?=E9=95=9C=E5=83=8F=E8=BD=AE=E6=8D=A2/=E6=8D=A2=E5=A4=B4?= =?UTF-8?q?=E5=9D=87=E4=B8=8D=E8=BF=87)=E5=88=A4=E6=95=8C=E5=AF=B9?= =?UTF-8?q?=E5=BC=83=E9=87=87;zt=5Fpool=2083=E5=8F=AA/zbgc=206=E5=8F=AA/dt?= =?UTF-8?q?gc=202=E5=8F=AA=E7=9C=9F=E5=AE=9E=E8=A1=8C=E6=95=B0=E5=AE=9E?= =?UTF-8?q?=E8=AF=81,dtgc=E7=A9=BA=E6=95=B0=E6=8D=AE=3D=E5=BD=93=E6=97=A5?= =?UTF-8?q?=E7=9C=9F=E6=97=A0=E8=B7=8C=E5=81=9C=E8=B5=B0=E6=97=A2=E6=9C=89?= =?UTF-8?q?=E5=86=99=E7=A9=BA+done=E8=AF=AD=E4=B9=89;=E4=B8=89=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E8=BF=9BPER=5FDATE=5FTYPES=E5=AE=B6=E6=97=8F(ak-event?= =?UTF-8?q?s=2019:30=E5=90=8C=E7=AA=97+3=E5=8D=95=E6=AC=A1=E8=B0=83?= =?UTF-8?q?=E7=94=A8<1min),=E9=9B=B6=E6=96=B0schtask=E9=9B=B6=E6=96=B0?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E9=9B=B6=E6=96=B0=E4=BE=9D=E8=B5=96;+4?= =?UTF-8?q?=E6=B5=8B=E8=AF=95(=E6=B3=A8=E5=86=8C/None=E5=A5=91=E7=BA=A6/df?= =?UTF-8?q?=E5=A5=91=E7=BA=A6/=E7=9C=9F=E7=A9=BAdf=E8=90=BD=E7=9B=98)=20[v?= =?UTF-8?q?ps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/data_platform/ak_events_wrapper.ps1 | 5 +-- .../data_platform/akshare_static_download.py | 36 ++++++++++++++++++- .../test_akshare_static_download.py | 34 ++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/scripts/data_platform/ak_events_wrapper.ps1 b/scripts/data_platform/ak_events_wrapper.ps1 index 8d8dfcd..a8c217e 100644 --- a/scripts/data_platform/ak_events_wrapper.ps1 +++ b/scripts/data_platform/ak_events_wrapper.ps1 @@ -1,5 +1,6 @@ # ak_events_wrapper.ps1 — sanguo-ak-events schtask wrapper (daily 19:30 akshare 事件类 per-date 当日) -# dragon_tiger/block_trade/margin_sse/restricted --start today --end today; per-date 每类1 unit 快 +# dragon_tiger/block_trade/margin_sse/restricted/zt_pool 三件套 --start today --end today; +# per-date 每类1 unit 快; zt_pool 系 (2026-09-02 双机实测 push2ex 集群稳定) 供情绪/打板因子 $env:http_proxy = '' $env:https_proxy = '' $env:all_proxy = '' @@ -9,5 +10,5 @@ $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,margin_sse,restricted --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,margin_sse,restricted,zt_pool,zt_pool_zbgc,zt_pool_dtgc --start $today --end $today *>> $log exit $LASTEXITCODE diff --git a/scripts/data_platform/akshare_static_download.py b/scripts/data_platform/akshare_static_download.py index fcc49c5..45cbf36 100644 --- a/scripts/data_platform/akshare_static_download.py +++ b/scripts/data_platform/akshare_static_download.py @@ -143,12 +143,19 @@ PER_STOCK_TYPES = ( "financial_abstract", # stock_financial_abstract(symbol="600519") # top_holders 单独 (per-stock × per-period) ) -# 模式 B: per-date 类型 (4 类, margin_szse 跳过) +# 模式 B: per-date 类型 (7 类, margin_szse 跳过) PER_DATE_TYPES = ( "dragon_tiger", # stock_lhb_detail_em(start_date, end_date) "block_trade", # stock_dzjy_mrmx(symbol="A股", start_date, end_date) "margin_sse", # stock_margin_detail_sse(date) "restricted", # stock_restricted_release_detail_em(start_date, end_date) + # 涨停池三件套 (2026-09-02 双机实测 push2ex 集群稳定: 连打多次全 OK, 与 push2 + # clist 集群的 IP 长窗暗配额无关)。情绪周期/打板因子原料; dtgc 空数据=当日 + # 真无跌停 (写空+done 语义正确); 三端点均只支持最近 ~30 个交易日, 历史回补 + # 只能往前 30 日。 + "zt_pool", # stock_zt_pool_em(date) 涨停池 + "zt_pool_zbgc", # stock_zt_pool_zbgc_em(date) 炸板池 + "zt_pool_dtgc", # stock_zt_pool_dtgc_em(date) 跌停池 ) # 模式 C: per-period 类型 (2 类) PER_PERIOD_TYPES = ( @@ -611,6 +618,30 @@ def fetch_restricted(date: str) -> Optional[pd.DataFrame]: return df +def fetch_zt_pool(date: str) -> Optional[pd.DataFrame]: + """stock_zt_pool_em(date=date) — 涨停池 (连板数/封板资金/首次封板时间等)。""" + df, _status = call_ak_with_retry( + ak.stock_zt_pool_em, f"zt_pool/{date}", date=date, + ) + return df + + +def fetch_zt_pool_zbgc(date: str) -> Optional[pd.DataFrame]: + """stock_zt_pool_zbgc_em(date=date) — 炸板池 (涨停开板失败股)。""" + df, _status = call_ak_with_retry( + ak.stock_zt_pool_zbgc_em, f"zt_pool_zbgc/{date}", date=date, + ) + return df + + +def fetch_zt_pool_dtgc(date: str) -> Optional[pd.DataFrame]: + """stock_zt_pool_dtgc_em(date=date) — 跌停池 (空=当日真无跌停, 写空+done)。""" + df, _status = call_ak_with_retry( + ak.stock_zt_pool_dtgc_em, f"zt_pool_dtgc/{date}", date=date, + ) + return df + + # ======================== per-period fetch 函数 (2 类) ======================== def fetch_forecast(period: str) -> Optional[pd.DataFrame]: @@ -1051,6 +1082,9 @@ def run_type_dispatch( "block_trade": fetch_block_trade, "margin_sse": fetch_margin_sse, "restricted": fetch_restricted, + "zt_pool": fetch_zt_pool, + "zt_pool_zbgc": fetch_zt_pool_zbgc, + "zt_pool_dtgc": fetch_zt_pool_dtgc, } units = build_per_date_units(t, fetch_map[t], args) return run_one_type(t, units, args) diff --git a/tests/data_platform/test_akshare_static_download.py b/tests/data_platform/test_akshare_static_download.py index 1a16e73..992fd8d 100644 --- a/tests/data_platform/test_akshare_static_download.py +++ b/tests/data_platform/test_akshare_static_download.py @@ -99,3 +99,37 @@ class TestFailedNoOverwrite: lambda *a, **k: (df, "ok")) assert mod.fetch_income_sheet("SH600519") is df assert mod.fetch_express("20260630") is df + + +class TestZtPoolFamily: + """涨停池三件套 (2026-09-02 双机实测入列, push2ex 集群)。""" + + def test_registered_as_per_date_types(self): + for t in ("zt_pool", "zt_pool_zbgc", "zt_pool_dtgc"): + assert t in mod.PER_DATE_TYPES + assert t in mod.ALL_TYPES + + def test_fetchers_none_on_retry_exhausted(self, monkeypatch): + monkeypatch.setattr( + mod, "call_ak_with_retry", lambda *a, **k: (None, "failed")) + assert mod.fetch_zt_pool("20260901") is None + assert mod.fetch_zt_pool_zbgc("20260901") is None + assert mod.fetch_zt_pool_dtgc("20260901") is None + + def test_fetchers_df_on_ok(self, monkeypatch): + df = pd.DataFrame({"连板数": [3]}) + monkeypatch.setattr(mod, "call_ak_with_retry", + lambda *a, **k: (df, "ok")) + assert mod.fetch_zt_pool("20260901") is df + assert mod.fetch_zt_pool_zbgc("20260901") is df + assert mod.fetch_zt_pool_dtgc("20260901") is df + + def test_empty_dtgc_day_writes_empty_and_marks(self, tmp_path, monkeypatch): + """跌停池 0 跌停日 = 真空 df → 写空 + done (复跑不重拉)。""" + monkeypatch.setattr(mod, "OUT_DIR", tmp_path) + status, _ = mod.download_one_unit( + "zt_pool_dtgc", "20260901_zt_pool_dtgc", + lambda: pd.DataFrame(), force=True) + assert status == "empty" + assert (tmp_path / "zt_pool_dtgc" / "20260901_zt_pool_dtgc.parquet").exists() + assert (tmp_path / "zt_pool_dtgc" / ".20260901_zt_pool_dtgc.akshare").exists()