feat(corpus): 巨潮504当日尾重试——主pass未done单元隔10min重走一轮,墙钟无余量跳过次日自愈兜底;spec§19.11.1 backlog执行记录 [nas]
CI/CD / test (push) Failing after 14m37s
CI/CD / nas-deploy (push) Has been skipped
CI/CD / nas-verify (push) Has been skipped

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 12:11:15 +08:00
parent f78e8f1349
commit 29e3ecbe2d
3 changed files with 133 additions and 0 deletions
@@ -944,6 +944,33 @@ c. **dividend 增量收编(轻量)**: legacy Day2b 快照(2020→2026-07)继续
年度集中(4-7 月),**年度刷新比每日增量合身**——2027-01 再议把年度刷新挂进 baostock
年度任务;当期零动作。
### 19.11.1 backlog 执行记录(2026-09-09,三项闭环)
- **a. valuation_baostock 2026 H1 回补 ✅** (150465a): `bs_eod.py --backfill-valuation
START END`——只拉估值 9 列(无 OHLCV,不动 dbbardata);名单=全A含退市∪query_all_stock
(窗口中点日)时点快照;断点序=buffer 落盘先、done 后写(崩=整批重拉幂等);终局
merge_valuation_frames(keep='last' 同 upsert_daily 语义)+os.replace 原子写回;中间产物
落 `data/_backfill_valuation/{tag}/`(独立目录,避开 sync_valuation_daily 的 scp -r
整目录拉取面)。09-09 09:35 schtask 一趟 7545 发零失败 82min,merge 93,678→**1,131,834
行键唯一**;验收=600519 锚 165 行 peTTM≈20.2 对公开口径/H1 日均 7062 行(含退市名单,
大于现存 5216 属预期)/10 列 schema 不变。一次性 schtask 已删,buffer 已清;NAS 由
03:09 链自动传播(09-10 晨到)。
- **b. forecast/express 补史 ✅** (f78e8f1): DOMAINS 扩两域;**边界探针实测 forecast
2003 年报起(458 行)/express 2005 年报起(133 行),2002 及更早东财返空**——per-domain
`BACKFILL_FROM{balance:2009,forecast:2003,express:2005}`+`backfill_from_year()`
(--start-year 显式覆盖优先)。**键分域**: forecast 4 键 (SECURITY_CODE,REPORT_DATE,
NOTICE_DATE,PREDICT_FINANCE_CODE)——同股同期同披露日多指标行(EPS 003+净利 004
并存)+多次修正史全保留,2019 年报 3646 行实测唯一;express 3 键(含 NOTICE_DATE,
2528 行实测唯一);两域 NOTICE_DATE 全期实测非空。verify_dmsk 同步五域键唯一。
09-09 11:51 手动触发 NAS run_dmsk.sh(日常 07:10 DSM 班次日自动接续);daily lane
识别 forecast 4897 行与探针精确一致。读法不变:基线层为主、dmsk 层补缺。
- **c. dividend 收编 ✅零动作备案**: legacy Day2b 快照继续用;2027-01 再议年度刷新挂
baostock 年度任务。
- **晨间三验收(09-09)**: xt_eod 09-07 缺口自愈绿(3353/4012/39 对齐 09-04 形态);
09-08 只补 30%(1040/1122/39)由 LOOKBACK=30 窗口当晚再扫自愈;dmsk 07:10 首个全自动
班绿(daily 178s/9 unit/108 发零失败);**巨潮 504 连续三日抖动(failed 18→150→113,
全 ann 单元网关 504,<1%,不标 done 次日重试不丢数据)——已升级报用户待拍板**。
## 参考(调查来源)
- xtdata 官方:https://dict.thinktrader.net/nativeApi/xtdata.html
+53
View File
@@ -77,6 +77,7 @@ PDF_PAUSE_FREE_GB = 200 # 容量闸门: 卷空闲低于此 → 暂停 PD
PDF_CAPACITY_CHECK_GB = 50 # 每落 50GB 自查一次 df
RATE = {"cninfo": 1.0, "eastmoney": 1.0, "article": 2.0} # 每域最小间隔(秒), 慢爬纪律
ANN_RETRY_WAIT_S = 600.0 # 巨潮 504 尾重试前等待(网关抖动恢复窗; 测试归零)
JITTER = (0.0, 0.3)
UA = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
@@ -785,6 +786,57 @@ def _run_ann_increment(ctx, pool, cl_cn, ledgers, recents):
return candidates
def _run_ann_tail_retry(ctx, pool, cl_cn, ledgers, recents, candidates):
"""巨潮 504 当日尾重试(spec §19.11.1 方案B, 2026-09-09 用户拍板):
主 pass 未 done 的 ann 单元(504/5xx 单元失败或域冷却早退跳过的)等
ANN_RETRY_WAIT_S 后重走一轮——巨潮网关间歇抖动, 当日找回, 不再全靠次日自愈。
墙钟无余量(deadline 已过)跳过=次日自愈兜底; 冒烟(--limit)不重试;
主 pass 全 done 零开销(不等待)。"""
if ctx.limit is not None:
return
ctx.commit() # 先刷 pending marker 落盘, is_done 才能看到主 pass 已完成单元
today = dt.date.today()
pending = [(c, o) for c, o in pool
if not is_done(ctx.lane, "ann", _dunit(c, today))]
if not pending:
return
if ctx.deadline and dt.datetime.now() >= ctx.deadline:
log.info("ann 尾重试: %d 单元未 done, 但墙钟已到(%s), 次日自愈",
len(pending), ctx.deadline.strftime("%H:%M"))
return
log.info("ann 尾重试: %d 单元未 done, 等待 %ds 后重走",
len(pending), int(ANN_RETRY_WAIT_S))
time.sleep(ANN_RETRY_WAIT_S)
ctx.reset_stage()
first_seen = today.isoformat()
for code, org in pending:
if ctx.budget_exhausted():
return
try:
rows = fetch_cninfo_announcements(cl_cn, code, org,
(today - dt.timedelta(days=1)).isoformat(),
today.isoformat())
except DomainCooldown as e:
log.warning("ann 尾重试 域冷却 @%s: %s", code, e)
ctx.rate_limited |= e.rate_limited
ctx.hard_cool |= not e.rate_limited
return
except (TransportError, HttpDeterministicError) as e:
ctx.failed += 1
log.warning("ann 尾重试 %s 失败(不标done): %s", code, e)
continue
candidates.extend(collect_pdf_candidates(rows))
norm = [norm_ann_row(r, first_seen) for r in rows]
new = _absorb_new("ann_meta", norm, ledgers, recents)
if new:
for r in new:
_record_depth(ctx, "ann", r["sec_code"] or code,
(r["ann_time"] or "")[:10])
log.info("ann 尾重试 %s: +%d/%d", code, len(new), len(rows))
ctx.unit_done("ann", _dunit(code, today), mark=ctx.limit is None)
ctx.stop_now()
def _run_news_increment(ctx, pool, cl_em, ledgers, recents, new_articles):
ctx.reset_stage()
today = dt.date.today()
@@ -948,6 +1000,7 @@ def run_daily(ctx, pool):
_reconcile_ledgers(ledgers, recents)
_prune_daily_increment_markers()
candidates = _run_ann_increment(ctx, pool, cl_cn, ledgers, recents)
_run_ann_tail_retry(ctx, pool, cl_cn, ledgers, recents, candidates)
_run_news_increment(ctx, pool, cl_em, ledgers, recents, new_articles := [])
_run_fulltext(ctx, cl_art, ledgers, recents, new_articles)
_run_pdf_daily(ctx, pool, cl_cn, ledgers, candidates)
@@ -22,6 +22,7 @@ def _fast(monkeypatch):
"""限速归零 + 抖动归零: 测试不 sleep。"""
monkeypatch.setattr(cd, "RATE", {k: 0.0 for k in cd.RATE})
monkeypatch.setattr(cd, "JITTER", (0.0, 0.0))
monkeypatch.setattr(cd, "ANN_RETRY_WAIT_S", 0.0)
@pytest.fixture
@@ -544,6 +545,58 @@ def test_daily_domain_cooldown_skips_domain_but_others_run(root, monkeypatch):
assert not cd.is_done("daily", "ann", "000001") # 巨潮域整体未标
def test_ann_tail_retry_recovers_failed_units(root, monkeypatch):
"""巨潮 504 主 pass 失败 → 当日尾重试一轮找回(用户拍板方案B, spec §19.11.1):
done 单元隔 ANN_RETRY_WAIT_S 重走, 数据当日入账并补 done"""
import pandas as pd
_wire_daily(monkeypatch, root)
calls = {"n": 0}
def flaky_504(client, code, org, start, end, category=""):
calls["n"] += 1
if calls["n"] <= 2: # 主 pass 两股都炸(网关抖动形态)
raise cd.TransportError("HTTP 504")
return [_ann_raw(aid=f"a{code}", code=code)]
monkeypatch.setattr(cd, "fetch_cninfo_announcements", flaky_504)
rc = cd.run_lane("daily")
assert rc == 1 # 失败发生过, rc 如实留痕
today = dt.date.today().isoformat()
ann = pd.read_parquet(root / "ann_meta" / f"dt={today}" / "part-0.parquet")
assert {"a000001", "a600519"} <= set(ann["announcement_id"]) # 尾重试当日找回
assert cd.is_done("daily", "ann", f"dt={today}/000001")
assert cd.is_done("daily", "ann", f"dt={today}/600519")
def test_ann_tail_retry_skipped_when_no_pending(root, monkeypatch):
"""主 pass 全 done → 零重试开销(不等待不重走)。"""
_wire_daily(monkeypatch, root)
slept = []
monkeypatch.setattr(cd.time, "sleep", lambda s: slept.append(s))
assert cd.run_lane("daily") == 0
assert slept == []
def test_ann_tail_retry_skipped_when_past_deadline(root, monkeypatch):
"""墙钟无余量(deadline 已过) → 跳过重试, 次日自愈兜底。"""
_wire_daily(monkeypatch, root)
calls = {"n": 0}
def always_504(client, code, org, start, end, category=""):
calls["n"] += 1
raise cd.TransportError("HTTP 504")
monkeypatch.setattr(cd, "fetch_cninfo_announcements", always_504)
slept = []
monkeypatch.setattr(cd.time, "sleep", lambda s: slept.append(s))
rc = cd.run_lane("daily", until="00:00") # deadline=今天 00:00 已过
assert rc == 1
assert slept == [] # 不等待
n_main = calls["n"] # 主 pass 2 发全炸
# 不再新增 retry 发数: 不重走
assert calls["n"] == n_main
def test_daily_429_gives_rc2(root, monkeypatch):
monkeypatch.setattr(cd, "load_stock_pool", lambda **kw: POOL)
monkeypatch.setattr(cd, "fetch_cninfo_announcements",