4 Commits

10 changed files with 275 additions and 8 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -202,7 +202,7 @@
| 因子贡献分解 | `sanguo_factor/attribution.py` | 日度 Fama-MacBeth 回归式归因(P3 第三刀, 流水线 spec §4.7 五条): 0.7 族代表+控制集(市场/log市值/行业哑变量)+sqrt(市值) WLS; 恒等式可加和+size 泄漏锚点+ridge 对照(λ=1 固定档, **惩罚随列方差缩放 A+λ·diag(A)≡相关形阵+λI——尺度不变, 等权/√市值两通道同效**; 09-27 审计 F-2 修订: 旧 λI 加在权重放大的原始阵上, √市值量级相对收缩 ~1e-9=no-op; 系数统一收缩 1/(1+λ), 仅 VIF>10 触发, 当前 quant12 vif≈1.4 不进通道故历史产物不受影响); 行业表 join 不中→industry_code=0 独立桶(显式 ind_0 哑变量, 0 桶最大时自适应基线), unknown_industry_share 披露——不静默丢样本(2026-09-26 data 契约; 表交付 .SH/.SZ 通用形态, 消费端 _norm_symbol 归一到 vt 形); 产物 attribution_<end>.json 供 /pipeline/attribution factorContribution 消费 |
| 归因批(月度 stage4) | `sanguo_factor/attribution_batch.py` | 09-27 落地、10-04 首班起挂月度批评链第四段(runner=`scripts/nas_sync/run_factor_monthly_standalone.sh`): ①15 截面增量前置(12 quant 源+size_log+在位者 composite_quant12_v2a_szneul+挑战者 composite_quant12_szneul, daily_section 220 历日短窗幂等 merge——NAS 截面无每日班, 批前必然新鲜; as_of 撞周末先归一 ≤as_of 最近 bar 日, `latest_bar_date` 库内权威防 sanity raise) ②归因双跑同窗同参(全窗锚 2021-07-01, 挑战者受其截面自然窗限制; 挑战者落工作区 challenger/ 子目录=端点 glob 非递归防顶掉) ③cp 双投递生效位(在位者件→`data/attribution/` 根 glob 取最新; 挑战者件→`challenger/attribution_<end>_<因子名>.json` 带因子名)——#69 影子 A/B 四周对照的归因侧供给; 工作区 `/volume1/stock/factor_cross_section/attribution/` 留全历史、生效位只持最新; fail loud=截面断供/归因挂/投递缺任一→容器非零→班次 final=1(静默旧件=红线) |
| 研判卡回放 | `sanguo_factor/verdict_replay.py` | 月度批评同班车:monthly_points 触发点普查(已晋级逐月 t 中位数<集体地板)+七档事后近似成绩(基准回测曲线等权混合,档3/4=需精确验证);产物 verdict_replay.json 供 `/pipeline/research-card` 拼卡(设计=流水线 spec §4.7 P3 第一刀,2026-09-25) |
| 先批后评·第一段 | `sanguo_factor/monthly_batch.py` | 月度批评日全量重评批薄 CLI(注册表全量→12M 窗→复用 run_batch_eval),跑批从「人点」变「日历」(决议 J) |
| 先批后评·第一段 | `sanguo_factor/monthly_batch.py` | 月度批评日全量重评批薄 CLI(注册表全量→12M 窗→复用 run_batch_eval),跑批从「人点」变「日历」(决议 J);**2026-10-10 加 `--daily`**(决议 H 修订:as_of 直用不归一月末,label=`daily_` 前缀;夜班批 as-of=T-1 双机 21:30,新因子出生次日得 t=盲区 1 月→1 天;判定/报告层月末快照语义不动=月度链照旧) |
| 数据缺口比对 | `sanguo_factor/data_gap_check.py` + `config/data_manifest.yaml` | 注册表 params.source vs 底座 manifest,缺口 exit 2 开 issue 转 data session(决议 J/§11.3-②) |
数据链: eval_store 的 metrics["1"]["t_stat"] 即衰减判定输入(daily_section 日度落库已在产原料)。部署接线(NAS DSM 任务/VPS schtask/对拍汇集)等用户在场执行,步骤在计划档尾节。
@@ -61,6 +61,7 @@ F 环是「管道」与「飞轮」的分界:归因发现→可能=衰减触
- **双机对拍=验证校准期机制(非永久双跑)**:对拍目的是验证与校准(防「当时就是错的」),不是刚需;首班 10-04 双机同评已双绿(一致 9/分歧 0),日度化后再跑**连续 4 周日度对拍关键指标(IC/t/rank)一致**即宣告验证通过→**VPS 独立日度跑**;NAS 降为**月度抽检对拍**(每月末快照对拍一次),「分歧超阈→不出降权告警、只开排查 issue(宁可误报排查,不可错发告警)」机制永久保留;
- **出生盲区消解(本修订最大红利)**:新因子出生当夜/次日自动搭上日班车拿到 t,盲区从最长一个月缩到一天——「出生预考」议题不再需要;
- **性能注记**:月度冷跑实测 40min 级;日度滚动下 12M 窗每天只滚一天,历史因子值全命中三级缓存(h5→md5 pkl),只有新截面要算,预期十几分钟级;首月实测校准,上限=40min 也放得下夜班窗口。
- **调度实配(2026-10-10 落地)**:`monthly_batch --daily` 开关(as_of 直用不归一月末,label=`daily_` 前缀;月度链缺省行为不变)。双机日度任务 21:30(VPS schtask `sanguo-factor-daily`=XML Daily,wrapper=`scripts/factor_research/run_factor_daily_wrapper.ps1` 两段链:daily 批+`monthly_review` 日度对拍件;NAS DSM 任务每日 21:30,runner=`scripts/nas_sync/run_factor_daily_standalone.sh` 一次性容器同款)。**registry 对齐修正(存量错位)**:批侧(日度+月度)`--registry` 从 `config/` 种子改读 `data/` 运行副本——与控制台同源,decompose 新生/判死即时进批视野(旧错位=首班后出生的因子月度批不认识、墓园因子照常占算力;防御=运行副本缺失时从种子拷)。**日度对拍件落 `daily/` 子目录**(判定层端点只读根层,月末快照语义零改);月度四段链照旧每月第一个周日跑(正式报告/归因/缺口开单权威不动,①段重跑幂等)。对拍自动化(NAS 拉 VPS 件+`review_compare` 台账)=验证期工具,后续小班补。
- **衰减判定(决议 F,只对已晋级因子)**:
| 条件 | 起步默认 | 防什么 |
+10 -4
View File
@@ -81,7 +81,7 @@ def resolve_with_yaml(registry: dict):
def plan_batch(registry: dict, as_of: str, window_months: int = 12,
resolver=None) -> dict[str, Any]:
resolver=None, daily: bool = False) -> dict[str, Any]:
"""注册表→批参数(main 只做 IO 编排,可单测).
「纯函数」契约勘正(P3-12): resolver=get_factor(默认)时纯读;
@@ -94,14 +94,17 @@ def plan_batch(registry: dict, as_of: str, window_months: int = 12,
曾漂移 5 名(fa_gross_margin/s0* vs 库名),月度批评会空转.
"""
resolve = resolver or get_factor
end = month_end(as_of)
# 日度化(决议 H 修订):daily 时 as_of 直用(T-1 夜班),不归一月末;
# 月度缺省行为不变(月末快照=判定层语义,月度链照旧归一).
end = as_of if daily else month_end(as_of)
names = sorted(n for n, e in registry["factors"].items()
if e["status"] not in EXCLUDED_STATUSES)
skipped = [n for n in names if resolve(n) is None]
return {"factor_names": [n for n in names if resolve(n) is not None],
"skipped_unregistered": skipped,
"start": window_start(end, window_months),
"end": end, "label": f"monthly_{as_of[:7]}"}
"end": end,
"label": (f"daily_{as_of}" if daily else f"monthly_{as_of[:7]}")}
def decomposer_new_names(registry: dict, factor_names: list[str]) -> list[str]:
@@ -152,10 +155,13 @@ def main(argv: list[str] | None = None) -> int:
ap.add_argument("--fund-data-dir", default=None,
help="财务静态域根目录 override(VPS 镜像路径与 NAS 默认各异时直传,"
"缺省走 cfg/默认解析)")
ap.add_argument("--daily", action="store_true",
help="日度模式(决议 H 修订):as_of 直用不归一月末,"
"label=daily_ 前缀;夜班批 as-of=T-1")
args = ap.parse_args(argv)
registry = load_registry(args.registry)
plan = plan_batch(registry, args.as_of, args.window_months,
resolver=resolve_with_yaml(registry))
resolver=resolve_with_yaml(registry), daily=args.daily)
if not plan["factor_names"]:
print("[monthly_batch] 注册表无可跑因子(全 retired/graveyard?)", file=sys.stderr)
return 1
@@ -31,13 +31,20 @@ Set-Location C:\sanguo_vnpy_v2
$PY = 'C:\Python310\python.exe'
$EVAL_DB = 'C:\sanguo_vnpy_v2\data\factor_eval.db'
$REGISTRY = 'C:\sanguo_vnpy_v2\config\factor_registry.yaml'
# 2026-10-10 registry aligned with console runtime copy (data/, not config seed):
# decomposer newborns / graveyard verdicts take effect in the next batch view.
$REGISTRY = 'C:\sanguo_vnpy_v2\data\factor_registry.yaml'
$MANIFEST = 'C:\sanguo_vnpy_v2\config\data_manifest.yaml'
$VNDB = 'C:\sanguo_vnpy_v2\data\quant_trading.db'
$FUND_DIR = 'C:\sanguo_vnpy_v2\data\static'
$OUT_DIR = 'C:\sanguo_vnpy_v2\reports\factor_monthly'
$LOG = Join-Path $OUT_DIR 'factor_monthly_cron.log'
# runtime copy defense: seed from config if missing (machine never ran the API)
if (-not (Test-Path $REGISTRY)) {
Copy-Item 'C:\sanguo_vnpy_v2\config\factor_registry.yaml' $REGISTRY
}
# as_of = last day of previous natural month
$first = Get-Date -Day 1
$asof = $first.AddDays(-1).ToString('yyyy-MM-dd')
@@ -0,0 +1,64 @@
# register_factor_daily_task.ps1 -- register schtask sanguo-factor-daily on VPS
# (2026-10-10 pipeline spec sec 4.2 decision H revision: cadence monthly -> daily)
#
# Trigger: DAILY 21:30 local. Rationale: the nightly batch evaluates as_of=T-1
# and must start only after the 21:00 xt_eod bridge has finished ingesting the
# T-1 session bars (21:00 -> ~21:10 done, 21:30 leaves margin).
# Sibling of sanguo-factor-monthly (first Sunday 09:30) which stays untouched:
# the monthly chain keeps producing official month-end snapshot reports.
#
# Run on VPS AFTER vps-deploy has placed the wrapper:
# powershell -ExecutionPolicy Bypass -File C:\sanguo_vnpy_v2\scripts\factor_research\register_factor_daily_task.ps1
# Verify afterwards: schtasks /query /tn sanguo-factor-daily /fo LIST /v
# (Next Run Time should be today/tomorrow 21:30.)
$env:http_proxy = ''
$env:https_proxy = ''
$env:all_proxy = ''
# ExecutionTimeLimit PT0S = no limit: cold full re-eval measured ~40min level;
# with rolling factor-value cache warm it is expected to drop to ~15min level
# (spec sec 4.2 performance note). IgnoreNew guards overlap with the monthly
# chain when first-Sunday 09:30 run spills (rare, batch is idempotent anyway).
$taskXml = @'
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>factor daily re-eval batch (monthly_batch --daily as_of=T-1 -&gt; monthly_review into daily/ subdir), 21:30 nightly after xt_eod</Description>
</RegistrationInfo>
<Triggers>
<CalendarTrigger>
<StartBoundary>2026-10-10T21:30:00</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Settings>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
</Settings>
<Actions Context="Author">
<Exec>
<Command>powershell</Command>
<Arguments>-ExecutionPolicy Bypass -File C:\sanguo_vnpy_v2\scripts\factor_research\run_factor_daily_wrapper.ps1</Arguments>
</Exec>
</Actions>
<Principals>
<Principal id="Author">
<UserId>S-1-5-18</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
</Task>
'@
$xmlPath = Join-Path $env:TEMP 'sanguo_factor_daily_task.xml'
$taskXml | Out-File -FilePath $xmlPath -Encoding Unicode
schtasks /create /tn sanguo-factor-daily /xml "$xmlPath" /f
Remove-Item $xmlPath -ErrorAction SilentlyContinue
Write-Output '=== VERIFY next run time (expect: today/tomorrow 21:30) ==='
schtasks /query /tn sanguo-factor-daily /fo LIST /v | Select-String -Pattern '下次运行时间|Next Run Time' | ForEach-Object { Write-Output (" {0}" -f $_.Line.Trim()) }
@@ -0,0 +1,60 @@
# run_factor_daily_wrapper.ps1 -- daily re-eval batch, VPS side (host=vps)
# (2026-10-10 pipeline spec sec 4.2 decision H revision: cadence monthly -> daily)
#
# Two stages only (the monthly 4-stage chain stays untouched and keeps
# running first-Sunday 09:30 for official month-end snapshot reports):
# 1) monthly_batch --daily as_of = T-1 directly (no month-end normalize),
# label=daily_YYYY-MM-DD
# 2) monthly_review daily compare artifact into daily/ subdir
# (judgement layer reads month-end files in the
# parent dir only -- snapshot semantics untouched)
#
# Schedule: daily 21:30 via register_factor_daily_task.ps1 (XML Daily trigger;
# 21:00 xt_eod must have finished ingesting T-1 bars first).
# rc semantics: stage1 non-zero -> stop (nothing to review).
# stage2 exit 2 = valid signal (alert), not a failure.
#
# Log: reports\factor_monthly\daily\factor_daily_cron.log
# NOTE: keep this file ASCII-only. PS 5.1 parses BOM-less files as GBK and
# multi-byte comment text can swallow statement lines (xt_eod lesson).
$env:http_proxy = ''
$env:https_proxy = ''
$env:all_proxy = ''
Set-Location C:\sanguo_vnpy_v2
$PY = 'C:\Python310\python.exe'
$EVAL_DB = 'C:\sanguo_vnpy_v2\data\factor_eval.db'
$REGISTRY = 'C:\sanguo_vnpy_v2\config\factor_registry.yaml'
$VNDB = 'C:\sanguo_vnpy_v2\data\quant_trading.db'
$FUND_DIR = 'C:\sanguo_vnpy_v2\data\static'
$OUT_DIR = 'C:\sanguo_vnpy_v2\reports\factor_monthly\daily'
$LOG = Join-Path $OUT_DIR 'factor_daily_cron.log'
# as_of = T-1 (yesterday): nightly batch evaluates up to last closed session
$asof = (Get-Date).AddDays(-1).ToString('yyyy-MM-dd')
if (-not (Test-Path $OUT_DIR)) { New-Item -ItemType Directory -Path $OUT_DIR -Force | Out-Null }
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LOG -Value "=== $stamp factor-daily start (as_of=$asof) ==="
& $PY -X utf8 -m sanguo_factor.monthly_batch --as-of $asof --daily --registry $REGISTRY --eval-db $EVAL_DB --vnpy-db $VNDB --fund-data-dir $FUND_DIR --report-dir $OUT_DIR --host vps *>> $LOG
$rcBatch = $LASTEXITCODE
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LOG -Value "=== $stamp stage1 daily_batch exit=$rcBatch ==="
if ($rcBatch -ne 0) {
Add-Content -Path $LOG -Value '!!! daily batch failed, stop (no fresh run for review) ==='
exit $rcBatch
}
& $PY -X utf8 -m sanguo_factor.monthly_review --as-of $asof --host vps --registry $REGISTRY --eval-db $EVAL_DB --out-dir $OUT_DIR *>> $LOG
$rcReview = $LASTEXITCODE
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LOG -Value "=== $stamp stage2 daily_review exit=$rcReview (2=alert signal, valid) ==="
$final = 0
if ($rcReview -eq 1) { $final = 1 }
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LOG -Value "=== $stamp factor-daily done final_rc=$final (batch=$rcBatch review=$rcReview) ==="
exit $final
@@ -0,0 +1,86 @@
#!/bin/bash
# run_factor_daily_standalone.sh — 因子日度重评批·NAS 侧(决议 H 修订 2026-10-10)
# 「bash wrapper → 一次性容器」同月度链模式(--user 1024:100 --group-add 101,
# 主容器重启/CI 重建永不波及)。
#
# 两段链(as_of=T-1,等 21:00 xt_eod 收完 T-1 bar,DSM 任务 21:30 触发):
# ① monthly_batch --daily 全量重评批(注册表全量,12M 窗,as_of 直用不归一月末)
# ② monthly_review 日度对拍件落 daily/ 子目录(host=nas,append-only)
# ——判定/报告层月末快照语义不动:月度四段链照旧每月第一个周日跑,正式报告
# 落根目录,API 端点只读根层,daily/ 子目录天然隔离。
#
# registry 对齐(2026-10-10 修正存量错位):批侧改读运行副本 /app/data/
# factor_registry.yaml(与控制台同源——decompose 新生/判死即时生效);
# 旧月度链用 config 种子,首班后新因子不在批视野=盲区不止时序还有注册表
# 错位。防御:运行副本缺失时从 config 种子拷(API 从未跑过的机器)。
#
# 部署位: NAS /volume1/stock/sanguo_vnpy_v2/run_factor_daily.sh(宿主文件,
# CI 不覆盖;改 repo 副本后 scp -O 手动同步,同月度纪律):
# scp -O scripts/nas_sync/run_factor_daily_standalone.sh \
# sanguo-nas:/volume1/stock/sanguo_vnpy_v2/run_factor_daily.sh
#
# DSM 任务(sanguo-factor-daily, 每日 21:30, 用户=admin):
# bash /volume1/stock/sanguo_vnpy_v2/run_factor_daily.sh
set -u
DOCKER=/var/packages/Docker/target/usr/bin/docker
BASE=/volume1/stock/sanguo_vnpy_v2
LOG=$BASE/factor_daily_cron.log
APP=/volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2
UID_ADMIN=1024
GID_ADMIN=100
GID_ADMINS=101
EVAL_DB=$BASE/data/factor_eval.db
OUT_DIR=$BASE/reports/factor_monthly/daily
REGISTRY=/app/data/factor_registry.yaml # 运行副本(控制台同源);缺失时下方向下拷种
# T-1(昨日):宿主 python3 算,不依赖 GNU date -d
AS_OF=$(python3 - <<'PY'
from datetime import date, timedelta
print((date.today() - timedelta(days=1)).isoformat())
PY
)
{
echo "=== $(date '+%F %T') factor-daily start (as_of=$AS_OF) ==="
mkdir -p "$OUT_DIR"
# 运行副本防御:缺失从 config 种子拷(API 从未起过的机器)
if [ ! -f "$APP/data/factor_registry.yaml" ]; then
echo "--- 运行副本缺失,从 config 种子拷贝 ---"
cp "$APP/config/factor_registry.yaml" "$APP/data/factor_registry.yaml" || exit 1
fi
"$DOCKER" run --rm --name sanguo-factor-daily --user "${UID_ADMIN}:${GID_ADMIN}" \
--group-add "${GID_ADMINS}" --no-healthcheck --entrypoint python \
-e HOME=/tmp -e MPLCONFIGDIR=/tmp/mpl \
-v /volume1/stock:/volume1/stock \
-v "$APP":/app:ro \
-w /app \
sanguo_vnpy_v2:lock-aligned \
-m sanguo_factor.monthly_batch --as-of "$AS_OF" --daily \
--registry "$REGISTRY" --eval-db "$EVAL_DB" --report-dir "$OUT_DIR" \
--host nas
rc_batch=$?
echo "=== $(date '+%F %T') stage1 daily_batch exit=$rc_batch ==="
if [ "$rc_batch" -ne 0 ]; then
echo "!!! stage1 失败,停链(review 无新批可评) ==="
exit "$rc_batch"
fi
"$DOCKER" run --rm --name sanguo-factor-daily-rev --user "${UID_ADMIN}:${GID_ADMIN}" \
--group-add "${GID_ADMINS}" --no-healthcheck --entrypoint python \
-e HOME=/tmp -e MPLCONFIGDIR=/tmp/mpl \
-v /volume1/stock:/volume1/stock \
-v "$APP":/app:ro \
-w /app \
sanguo_vnpy_v2:lock-aligned \
-m sanguo_factor.monthly_review --as-of "$AS_OF" --host nas \
--registry "$REGISTRY" --eval-db "$EVAL_DB" --out-dir "$OUT_DIR"
rc_review=$?
echo "=== $(date '+%F %T') stage2 daily_review exit=$rc_review (2=告警信号,有效) ==="
final=0
[ "$rc_review" -eq 1 ] && final=1
echo "=== $(date '+%F %T') factor-daily done final_rc=$final (batch=$rc_batch review=$rc_review) ==="
exit $final
} >> "$LOG" 2>&1
@@ -47,7 +47,9 @@ GID_ADMIN=100 # users
GID_ADMINS=101 # administrators(Synology ACL 授权组,金丝雀实测必带)
EVAL_DB=$BASE/data/factor_eval.db
OUT_DIR=$BASE/reports/factor_monthly
REGISTRY=/app/config/factor_registry.yaml
# 2026-10-10 registry 对齐控制台运行副本(data/,非 config 种子):decompose
# 新生/判死即时进批视野;防御=运行副本缺失时从种子拷(API 从未跑过的机器)
REGISTRY=/app/data/factor_registry.yaml
MANIFEST=/app/config/data_manifest.yaml
# 上月末(自然月末):宿主 python3 算,不依赖 GNU date -d
@@ -70,6 +72,12 @@ PY
echo "=== $(date '+%F %T') factor-monthly start (as_of=$AS_OF) ==="
mkdir -p "$OUT_DIR"
# 运行副本防御:缺失从 config 种子拷
if [ ! -f "$APP/data/factor_registry.yaml" ]; then
echo "--- 运行副本缺失,从 config 种子拷贝 ---"
cp "$APP/config/factor_registry.yaml" "$APP/data/factor_registry.yaml" || exit 1
fi
"$DOCKER" run --rm --name sanguo-factor-batch --user "${UID_ADMIN}:${GID_ADMIN}" \
--group-add "${GID_ADMINS}" --no-healthcheck --entrypoint python \
-e HOME=/tmp -e MPLCONFIGDIR=/tmp/mpl \
+34
View File
@@ -51,6 +51,40 @@ def test_plan_batch_excludes_dead_and_labels(registry):
assert plan["label"] == "monthly_2026-09"
def test_plan_batch_daily_uses_asof_direct(registry):
"""日度化(决议 H 修订):daily=True 时 as_of 直用不归一月末,label 前缀 daily_.
夜班批 as-of=T-1——若仍归一,10-09 会吸到 10-31(未来日),窗 end 越过
数据水位即盲区消解失效."""
plan = plan_batch(registry, "2026-10-09", daily=True)
assert plan["end"] == "2026-10-09"
assert plan["label"] == "daily_2026-10-09"
# 窗起点同款滚动(含 end 当月共 12M:2025-11 起算)
assert plan["start"] == "2025-11-01"
# 月度缺省行为不变(防回归)
monthly = plan_batch(registry, "2026-10-09")
assert monthly["end"] == "2026-10-31"
assert monthly["label"] == "monthly_2026-10"
def test_main_daily_flag_reaches_plan(monkeypatch, registry, tmp_path):
"""CLI --daily 透传 plan_batch(main 只做 IO 编排)."""
calls = {}
def fake_run(factor_names, start, end, eval_db, label, **kw):
calls.update(end=end, label=label)
return {"run_id": "x"}
monkeypatch.setattr(monthly_batch, "run_batch_eval", fake_run)
p = str(tmp_path / "reg.yaml")
save_registry(p, registry)
rc = main(["--as-of", "2026-10-09", "--daily", "--registry", p,
"--eval-db", "fake.db"])
assert rc == 0
assert calls["end"] == "2026-10-09"
assert calls["label"] == "daily_2026-10-09"
def test_main_invokes_run_batch_eval_with_plan(monkeypatch, registry, tmp_path):
calls = {}