From c8ba9ea15f2d7e9baefd06d856a31fc48c09ec55 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Wed, 7 Oct 2026 13:13:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(factor):=20=E7=BC=BA=E5=8F=A3=E5=BC=80?= =?UTF-8?q?=E5=8D=95=E6=9D=83=E5=A8=81=E5=8D=95=E6=9C=BA=E5=88=B6=E2=80=94?= =?UTF-8?q?=E2=80=94maybe=5Fopen=5Fgap=5Fissue=20=E5=A2=9E=20host=20?= =?UTF-8?q?=E5=8F=82=20vps=20=E4=BE=A7=E7=BB=93=E6=9E=84=E6=80=A7=E6=8B=92?= =?UTF-8?q?=E5=BC=80(NAS=20=E5=8D=95=E4=BE=A7=E6=9D=83=E5=A8=81,=E5=85=B3?= =?UTF-8?q?=E6=AD=BB=E5=8F=8C=E6=9C=BA=E5=90=8C=E6=97=A5=E7=AB=9E=E6=80=81?= =?UTF-8?q?=E5=8F=8C=E5=BC=80)+data=5Fgap=5Fcheck/monthly=5Fbatch=20CLI=20?= =?UTF-8?q?--host=20=E9=80=8F=E4=BC=A0+NAS=20sh/VPS=20ps1=20=E5=8F=8C=20wr?= =?UTF-8?q?apper=20=E5=A3=B0=E6=98=8E=E4=BE=A7=E5=88=AB=20(=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=20P3-14;runbook+=E5=9B=A0=E5=AD=90=E6=A1=A3=20=C2=A77?= =?UTF-8?q?.7)=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/deployment/vps-production-runbook.md | 2 +- docs/factor_research/factor-system-design.md | 2 +- sanguo_factor/data_gap_check.py | 14 +++++- sanguo_factor/monthly_batch.py | 5 +- .../factor_monthly_wrapper.ps1 | 6 ++- .../nas_sync/run_factor_monthly_standalone.sh | 6 ++- tests/factor/test_data_gap_check.py | 46 +++++++++++++++++++ tests/factor/test_monthly_batch.py | 2 +- 8 files changed, 73 insertions(+), 10 deletions(-) diff --git a/docs/deployment/vps-production-runbook.md b/docs/deployment/vps-production-runbook.md index 51a0f69c..30cb3c33 100644 --- a/docs/deployment/vps-production-runbook.md +++ b/docs/deployment/vps-production-runbook.md @@ -228,7 +228,7 @@ ssh 49.232.102.198 "powershell -Command \"Get-Content C:\sanguo_bigqmt\qmt_relog - **反向镜像链(融合 spec §20.11,2026-09-17 新增)**:`push_vps_mirror_daily.sh`+`vps_mirror_lib.py`(纯逻辑,宿主 python3.8 stdlib)——corpus **五子域**(09-22 起 +flash_meta 快讯域)+dmsk 五域 NAS→VPS **推送**(09-18 起挂 `sanguo-corpus-daily` DSM 任务尾部=`run_corpus_standalone.sh` EXIT trap 完赛即推,典型 ~10:1x/backfill 日 ~17:3x;rc 隔离=corpus rc∈0/1/3 才推[09-19 首验勘误:rc() 把「墙停+噪声 failed」报 1,旧 0/3 闸门 11 天只推 1 天,仅 rc=2 限流不推]、推送失败只记日志不进聚合 rc;**admin 账号**=避开 root 属主坑;原独立 16:00 DSM 方案作废),分区粒度 journal 指纹 diff 只增不删;部署位 `/volume1/stock/vps-mirror/`(宿主文件,改 repo 副本后 scp -O 同步,同 corpus/dmsk runner 纪律),journal/watermark_*.json/push.log 同目录;VPS 落 `data\corpus`+`data\fundamentals\dmsk`+族根 `_mirror_watermark.json`(消费方 `get_watermark()` 晨检对表);任一失败不 commit journal/不落水位线=次日 diff 自愈;`--dry-run` 打印将推送清单=验收收敛证明。设计与消费契约=融合 spec §20.11。09-20 增**独立 static manifest lane**(spec §20.11.7):`data_backup/static_overlay/manifests/*.list`(补洗变更清单,每行 `/`)文件级幂等推送、整单成功归档 pushed/、失败只 WARN 不影响 corpus/dmsk 聚合——三表 per-stock 文件由此 lane 推 VPS(不进 journal 指纹体系)。 - **flash 快讯 lane+xcheck 对账 lane(融合 spec §20.12.3/§20.12.4,09-22 上线)**:①`run_corpus_standalone.sh` 第三 lane `--lane flash`(金十快照+东财 kuaixun 翻页 T-1 边界,秒级完赛,rc 隔离不进聚合、独立跑不 gate 于 daily/backfill)→ corpus/flash_meta/ 落盘,镜像车五子域自动携带;②`run_corpus_standalone.sh` 第四 lane `xcheck.py --lane corpus_side`(ann_em 拉取+公告披露日对照+腾讯 qt 全市场估值快照对照,rc 隔离);③`run_dmsk.sh` 第四 lane `xcheck.py --lane fund_side`(读 wash manifests 当日命中股→新浪三表近 12 期+两源 NOTICE_DATE 对照)。xcheck 域落 `corpus/xcheck/` **不推 VPS**(NAS 侧质检设施);findings 报告+trace 追溯=融合 spec §20.12.4;容器名 sanguo-xcheck(杀=`docker stop sanguo-xcheck`)。首跑锚点:corpus cron.log `flash lane exit=0(隔离)` 行 + `xcheck corpus_side exit=0` 行 + dmsk cron.log `xcheck fund_side exit=0` 行 + 次日 push.log 出现 flash_meta 分区计数。 - **funnel 漏斗 lane+integrity gate lane(融合 spec §20.16/流水线 spec §4.8 P4-3,10-02 上线)**:`run_corpus_standalone.sh` 第五/六段——⑤`corpus_funnel.py --until 23:00 --llm-cap 200 --token-budget 3000000 --no-llm`(第一层词表八类事件零成本全量→events_llm 直落;第二层 LLM 只碰词表 miss:候选入 `state/funnel_pending.jsonl` 队列、串行慢爬、token 预算断路 rc=3 次夜续——**10-05 审计 P2-16 前该断路仅文档宣称未接线,现已实接 3M=每夜费用上界**(≈整夜墙钟吞吐量级,正常夜 <0.5M 不触;backlog 夜 checkpoint 次夜续);水位 `state/funnel_watermark.json` 只前向,历史回填 `--start` 按预算分批)⑥`integrity_gate.py --profile default`(三层门 crit exit1/warn exit2)。两段 rc 隔离不进聚合。**LLM env 三件与 P4-1 向导同款(§运维表 288 行)**:DSM 任务环境或 run_corpus.sh 头 export 后由 wrapper 条件传递进容器;缺 key 且当晚有候选 → funnel rc=1(fail loud,非故障面=未配 env)。冒烟:corpus cron.log `funnel exit=0` 行+`integrity gate exit=0` 行+`corpus/events_llm/dt=*/` 分区出现;`--no-llm` 可只验第一层。容器名 sanguo-funnel/sanguo-igate(杀=docker stop)。**10-05 用户拍板(B 案):LLM 层停用待令——`--no-llm` 已常态接线**(bigmodel 余额耗尽 429 熔断后裁决):词表层照跑零成本(events_llm 域 model="lexicon" 行继续落)、llm_calls=0、`funnel_pending.jsonl` 冻结只增不减(当夜新命中照常入队,LLM 不消费);开闸=去该 flag 续消化,`--token-budget 3M` 接线保留复用即生效。**P3-23(10-07):全 lane rc 分档打标**——每 lane exit 行后追加 `[verdict] lane=X rc=N outcome=语义词`(通用 0=ok/125=docker_daemon_down/137=sigkilled/130=interrupted/143=terminated;daily|backfill 1=noise_failed/2=rate_limited/3=wallclock_stop;funnel 1=fail_loud/3=checkpoint[锁让路/墙钟/预算次夜续];gate 1=crit/2=warn),值班/守望 grep 语义位不再裸读 exit 码——10-07 停电夜(daily 137+下游 125)需逐条人肉分诊的教训。 -- **因子月度批评·先批后评四段链(流水线 spec §4.2 决议 H+§12.4 决议 J,09-23 上线;09-27 加 stage4 归因批 10-04 首班起)**:双机同评——NAS+VPS 每月第一个周日 09:30 各跑同一批评链(①②③段),JSON 对拍件(`{host}_{asof}.json`)落各自报告目录,人工/VPS 侧 scp 汇集到 NAS 后 `python -m sanguo_factor.review_compare nas_*.json vps_*.json` 对拍(一致=落档进告警流程;分歧 exit 2=只开排查 issue 不出降权告警)。ⓐ**NAS 侧**:`run_factor_monthly.sh`(源码=repo `scripts/nas_sync/run_factor_monthly_standalone.sh`,改后 scp -O 同步生效位 `/volume1/stock/sanguo_vnpy_v2/`)——一次性容器四段串行 `monthly_batch`(注册表全量 12M 窗重评批,重活)→`monthly_review --host nas`(判定报告,JSON append-only)→`data_gap_check`→`attribution_batch`(归因批: 15 截面增量[12 源+size_log+在位者+挑战者,daily_section 幂等 merge,as_of 撞周末模块内归一最近 bar 日]→归因双跑[#69 影子 A/B 归因侧供给,挑战者落 challenger/ 子目录]→cp 双投递生效位[在位者件→admin 家 `data/attribution/` 根 glob 取最新;挑战者件带因子名;生效位目录容器挂 rw `/effective-data`,代码卷恒 :ro];工作区 `/volume1/stock/factor_cross_section/attribution/` 留全历史);as_of=上月末脚本头自算;rc 语义=①非零停链、②③ exit 2 是有效信号按 0 聚合、④非零=真失败(截面断供/归因挂/投递缺)进 final;容器金丝雀坑=**必须 `-e HOME=/tmp -e MPLCONFIGDIR=/tmp/mpl`**(--user 1024 下 HOME=/ → vnpy import 建不了 `/.vntrader`)。报告目录 `/volume1/stock/sanguo_vnpy_v2/reports/factor_monthly/`;日志=factor_monthly_cron.log。DSM 任务 `sanguo-factor-monthly`=**每周日 09:30 admin(UI 手建)+脚本头日号闸**——DSM 计划任务月度档只有「每月几号」没有「第一个周日」原生选项,调度只能建每周日,「是不是第一个周日」由脚本头 `dom>7 静默退出` 兜住。ⓑ**VPS 侧**:wrapper=`scripts/factor_research/factor_monthly_wrapper.ps1`(三段同链批评部分,`--host vps`,eval_db=`C:\sanguo_vnpy_v2\data\factor_eval.db`,报告/日志=`C:\sanguo_vnpy_v2\reports\factor_monthly\`;ASCII-only 纪律;**stage4 归因批不镜像**——归因双跑落 NAS 工作区+生效位,VPS 侧归因件走 infra 偏差日报链另行供给);schtask 注册=`scripts/factor_research/register_factor_monthly_task.ps1`(**XML ScheduleByMonthDayOfWeek 原生「每月第一个周日」**,无需日号闸——zh-CN schtasks 不吃星期名组合走 ak-monthly 同款 XML 路线;SYSTEM/不限时,monthly_batch 重活首班实测耗时后再定是否拆任务错时)——vps-deploy 把两文件推上 VPS 后 ssh 跑一次注册脚本即成。首班=部署后第一个周日人工盯全程。 +- **因子月度批评·先批后评四段链(流水线 spec §4.2 决议 H+§12.4 决议 J,09-23 上线;09-27 加 stage4 归因批 10-04 首班起)**:双机同评——NAS+VPS 每月第一个周日 09:30 各跑同一批评链(①②③段),JSON 对拍件(`{host}_{asof}.json`)落各自报告目录,人工/VPS 侧 scp 汇集到 NAS 后 `python -m sanguo_factor.review_compare nas_*.json vps_*.json` 对拍(一致=落档进告警流程;分歧 exit 2=只开排查 issue 不出降权告警)。ⓐ**NAS 侧**:`run_factor_monthly.sh`(源码=repo `scripts/nas_sync/run_factor_monthly_standalone.sh`,改后 scp -O 同步生效位 `/volume1/stock/sanguo_vnpy_v2/`)——一次性容器四段串行 `monthly_batch`(注册表全量 12M 窗重评批,重活)→`monthly_review --host nas`(判定报告,JSON append-only)→`data_gap_check`→`attribution_batch`(归因批: 15 截面增量[12 源+size_log+在位者+挑战者,daily_section 幂等 merge,as_of 撞周末模块内归一最近 bar 日]→归因双跑[#69 影子 A/B 归因侧供给,挑战者落 challenger/ 子目录]→cp 双投递生效位[在位者件→admin 家 `data/attribution/` 根 glob 取最新;挑战者件带因子名;生效位目录容器挂 rw `/effective-data`,代码卷恒 :ro];工作区 `/volume1/stock/factor_cross_section/attribution/` 留全历史);as_of=上月末脚本头自算;rc 语义=①非零停链、②③ exit 2 是有效信号按 0 聚合、④非零=真失败(截面断供/归因挂/投递缺)进 final;容器金丝雀坑=**必须 `-e HOME=/tmp -e MPLCONFIGDIR=/tmp/mpl`**(--user 1024 下 HOME=/ → vnpy import 建不了 `/.vntrader`)。报告目录 `/volume1/stock/sanguo_vnpy_v2/reports/factor_monthly/`;日志=factor_monthly_cron.log。DSM 任务 `sanguo-factor-monthly`=**每周日 09:30 admin(UI 手建)+脚本头日号闸**——DSM 计划任务月度档只有「每月几号」没有「第一个周日」原生选项,调度只能建每周日,「是不是第一个周日」由脚本头 `dom>7 静默退出` 兜住。ⓑ**VPS 侧**:wrapper=`scripts/factor_research/factor_monthly_wrapper.ps1`(三段同链批评部分,`--host vps`,eval_db=`C:\sanguo_vnpy_v2\data\factor_eval.db`,报告/日志=`C:\sanguo_vnpy_v2\reports\factor_monthly\`;ASCII-only 纪律;**stage4 归因批不镜像**——归因双跑落 NAS 工作区+生效位,VPS 侧归因件走 infra 偏差日报链另行供给);schtask 注册=`scripts/factor_research/register_factor_monthly_task.ps1`(**XML ScheduleByMonthDayOfWeek 原生「每月第一个周日」**,无需日号闸——zh-CN schtasks 不吃星期名组合走 ak-monthly 同款 XML 路线;SYSTEM/不限时,monthly_batch 重活首班实测耗时后再定是否拆任务错时)——vps-deploy 把两文件推上 VPS 后 ssh 跑一次注册脚本即成。首班=部署后第一个周日人工盯全程。10-07 起:双侧 wrapper 月度批/缺口比对调用均带 `--host nas|vps` 侧别声明(P3-14 缺口开单权威单机制——缺口开单仅 NAS 单侧权威,VPS 侧 `--host vps` 结构性拒开,即便日后两侧同配 token+开单 flag 也不会双机同日双开;NAS sh 生效位改动照旧 scp -O 同步,下次链班=11-01 第一个周日前完成即可)。 - **static 三表事件驱动补洗(融合 spec §20.11.7,2026-09-20 上线)**:`run_dmsk.sh` 第三 lane `static_overlay_wash.py`(dmsk daily/backfill 后 ~07:2x 起跑,--until 15:00 墙停,rc 隔离)——dmsk NOTICE_DATE 雷达命中股逐股 F10(emweb) 近 12 期 upsert 重写 `static/{income,balance,cashflow}/` per-stock 文件;pending 重试队列(F10 落库滞后/网络失败次日自愈,30 天老化);周日 sync_parquet 全量回冲=雷达窗内自愈、更深归月度全量洗兜底;变更清单→晚间镜像车 manifest lane 推 VPS。 - 数据归属口径四原则与 14 条管线全景=融合 spec §20.10(数据面唯一权威查询入口)。 diff --git a/docs/factor_research/factor-system-design.md b/docs/factor_research/factor-system-design.md index f0f5ba5f..955ebcd6 100644 --- a/docs/factor_research/factor-system-design.md +++ b/docs/factor_research/factor-system-design.md @@ -243,7 +243,7 @@ 审计杂项收敛批(2026-10-05,P3-15/16/17/18/19+T6-N1;上段 defer 台账「pre_gate_table 硬编码 2.0」就此收编):exam_gate 缺周期语义改 fail-closed——1/5/10d 任一周期任一侧缺 ICIR→INSUFFICIENT_PERIODS 不建议晋级(原「缺周期跳过不计」可退化单周期对比出 SUGGEST_PROMOTE,已废);考场报告文件名带 HHMMSS(同日重跑不覆盖,eval run label 不变);pre_gate REVIEW 阈值收编 _PROMOTION_BAR 常量(负 t 侧无镜像门边带=负向晋级不存在,维持现状);v2a 条目补 `evidence_run: ev_20260911_021832_2f68`(=promotion_t 26.32 锚批,NAS factor_eval.db label=composite-v2a-h2,1d t=26.3192 实查核验;**惯例:promoted 条目 promotion_t 必须可溯源到 evidence_run**);composite_library.build_composite_expression docstring 明示等权交集语义(任一源缺→null,分母恒 N 不重归一);data_gap_check 台账坏行(合法 JSON 非 dict)降级跳过+stderr(T6-N1)。 -开工批收编(2026-10-07,#91①⑤+P3-14;计划=plans/2026-10-07-factor-followup-batch.md):exam_gate 缺周期判定扩 isnan——任一侧 ICIR 为 NaN 视同缺失(部分 NaN=INSUFFICIENT_PERIODS、全 NaN=DATA_INSUFFICIENT),上段 defer 台账「首年校准加 isnan」就此提前闭合(#91⑤)。条目级 origin 写入方落地——upsert_factor 增 origin 参(合法 decomposer/manual;建条即落、同名幂等返回不覆盖、非法值拒),decompose 透传 origin=decomposer,P3-11 条目级分支由向前兼容位转正(人工改判退出=yaml 手改 origin: manual;§4.2 表两行同步勘正)(#91①)。 +开工批收编(2026-10-07,#91①⑤+P3-14;计划=plans/2026-10-07-factor-followup-batch.md):exam_gate 缺周期判定扩 isnan——任一侧 ICIR 为 NaN 视同缺失(部分 NaN=INSUFFICIENT_PERIODS、全 NaN=DATA_INSUFFICIENT),上段 defer 台账「首年校准加 isnan」就此提前闭合(#91⑤)。条目级 origin 写入方落地——upsert_factor 增 origin 参(合法 decomposer/manual;建条即落、同名幂等返回不覆盖、非法值拒),decompose 透传 origin=decomposer,P3-11 条目级分支由向前兼容位转正(人工改判退出=yaml 手改 origin: manual;§4.2 表两行同步勘正)(#91①)。缺口开单权威单机制(P3-14)——maybe_open_gap_issue 增 host 参,vps 侧结构性拒开(开单仅 NAS 单侧权威);data_gap_check/monthly_batch CLI 增 --host 透传,NAS sh 与 VPS ps1 wrapper 双侧声明侧别(台账仍本地 append-only,跨机第一道去重=Gitea 双层查询,本机制=第二道结构性闸)。 ## 8. 前端与任务态 diff --git a/sanguo_factor/data_gap_check.py b/sanguo_factor/data_gap_check.py index df3424c5..95dc16f2 100644 --- a/sanguo_factor/data_gap_check.py +++ b/sanguo_factor/data_gap_check.py @@ -160,14 +160,21 @@ def append_ledger(path: str, fingerprint: str, issue_number: int, def maybe_open_gap_issue(gaps: dict[str, list[str]], manifest_path: str, - ledger_path: str = GAP_ISSUE_LEDGER) -> int | None: + ledger_path: str = GAP_ISSUE_LEDGER, + host: str | None = None) -> int | None: """有 token 且同指纹无 open 单(台账+网络双层去重)才开;失败降级返 None. 去重三重(P2-13): ①本地台账命中→跳过不打网络;②open issue 双层查询 (首页 LIST+search 兜底)命中→跳过;③查询网络异常→跳过开单本月 (stderr 留痕,下月再试)——只有「确认不存在」才真正开单, 开单成功后回写台账。 + P3-14 开单权威单机制: host="vps" 结构性拒开(开单仅 NAS 单侧权威—— + 台账本地不跨机,双机同查同开窗口就此关死);host=None(人工运行)不限. """ + if host == "vps": + print("[data_gap_check] host=vps 非开单权威侧(P3-14 权威单机制:" + "NAS 单侧开单),跳过开单", file=sys.stderr) + return None token = os.environ.get("SANGUO_GITEA_TOKEN") if not token: return None @@ -198,6 +205,9 @@ def main(argv: list[str] | None = None) -> int: ap.add_argument("--gitea-open", action="store_true", help="有缺口时尝试开 Gitea issue(需 env SANGUO_GITEA_TOKEN;" "同指纹 open 单存在则跳过)") + ap.add_argument("--host", choices=("nas", "vps"), default=None, + help="运行侧别声明(P3-14 开单权威单机制: vps 侧结构性" + "拒开缺口单,NAS=权威侧;缺省=人工运行不限)") args = ap.parse_args(argv) if not os.path.exists(args.manifest): print(f"[data_gap_check] manifest 不存在: {args.manifest}", file=sys.stderr) @@ -212,7 +222,7 @@ def main(argv: list[str] | None = None) -> int: print(f"[data_gap_check] {len(gaps)} 因子有数据缺口(详见 data_gaps.json): " + ", ".join(sorted(gaps))) if args.gitea_open: - num = maybe_open_gap_issue(gaps, args.manifest) + num = maybe_open_gap_issue(gaps, args.manifest, host=args.host) if num: print(f"[data_gap_check] 已开数据缺口 issue #{num}") return 2 diff --git a/sanguo_factor/monthly_batch.py b/sanguo_factor/monthly_batch.py index 74d3c5c9..e1039d0f 100644 --- a/sanguo_factor/monthly_batch.py +++ b/sanguo_factor/monthly_batch.py @@ -143,6 +143,9 @@ def main(argv: list[str] | None = None) -> int: help="底座数据域清单(gap 比对供给侧)") ap.add_argument("--gap-issue", action="store_true", help="有数据缺口时尝试开 Gitea issue(决议 M;默认关)") + ap.add_argument("--host", choices=("nas", "vps"), default=None, + help="运行侧别声明(--gap-issue 开单权威单机制 P3-14: " + "vps 侧拒开,NAS=权威侧;缺省=人工运行不限)") ap.add_argument("--vnpy-db", default=None, help="行情库路径 override(双机 cfg 路径各异时直传," "缺省走 config 解析)") @@ -191,7 +194,7 @@ def main(argv: list[str] | None = None) -> int: + "; ".join(f"{f}→{','.join(s)}" for f, s in sorted(gaps.items())), file=sys.stderr) if args.gap_issue: - num = dgc.maybe_open_gap_issue(gaps, args.manifest) + num = dgc.maybe_open_gap_issue(gaps, args.manifest, host=args.host) if num: print(f"[monthly_batch] 已开数据缺口 issue #{num}") print(f"[monthly_batch] {len(plan['factor_names'])} 因子 " diff --git a/scripts/factor_research/factor_monthly_wrapper.ps1 b/scripts/factor_research/factor_monthly_wrapper.ps1 index 3c4d0377..f85ec304 100644 --- a/scripts/factor_research/factor_monthly_wrapper.ps1 +++ b/scripts/factor_research/factor_monthly_wrapper.ps1 @@ -21,6 +21,8 @@ # No SANGUO_GITEA_TOKEN needed here: the monthly chain never calls the Gitea # API (J1 issue opening lives in `python -m sanguo_factor.version_registry # transition --gitea-open`, a manual promotion action). +# 2026-10-07: --host vps also refuses gap-issue opening structurally +# (P3-14 single-side authority: only NAS opens gap issues). $env:http_proxy = '' $env:https_proxy = '' @@ -45,7 +47,7 @@ if (-not (Test-Path $OUT_DIR)) { New-Item -ItemType Directory -Path $OUT_DIR -Fo $stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' Add-Content -Path $LOG -Value "=== $stamp factor-monthly start (as_of=$asof) ===" -& $PY -X utf8 -m sanguo_factor.monthly_batch --as-of $asof --registry $REGISTRY --eval-db $EVAL_DB --vnpy-db $VNDB --fund-data-dir $FUND_DIR --report-dir $OUT_DIR *>> $LOG +& $PY -X utf8 -m sanguo_factor.monthly_batch --as-of $asof --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 monthly_batch exit=$rcBatch ===" @@ -59,7 +61,7 @@ $rcReview = $LASTEXITCODE $stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' Add-Content -Path $LOG -Value "=== $stamp stage2 monthly_review exit=$rcReview (2=alert/collective signal, valid) ===" -& $PY -X utf8 -m sanguo_factor.data_gap_check --registry $REGISTRY --manifest $MANIFEST --out-dir $OUT_DIR *>> $LOG +& $PY -X utf8 -m sanguo_factor.data_gap_check --registry $REGISTRY --manifest $MANIFEST --out-dir $OUT_DIR --host vps *>> $LOG $rcGap = $LASTEXITCODE $stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' Add-Content -Path $LOG -Value "=== $stamp stage3 data_gap_check exit=$rcGap (2=gap signal, valid) ===" diff --git a/scripts/nas_sync/run_factor_monthly_standalone.sh b/scripts/nas_sync/run_factor_monthly_standalone.sh index 72dec608..0919e64f 100644 --- a/scripts/nas_sync/run_factor_monthly_standalone.sh +++ b/scripts/nas_sync/run_factor_monthly_standalone.sh @@ -78,7 +78,8 @@ PY -w /app \ sanguo_vnpy_v2:lock-aligned \ -m sanguo_factor.monthly_batch --as-of "$AS_OF" \ - --registry "$REGISTRY" --eval-db "$EVAL_DB" --report-dir "$OUT_DIR" + --registry "$REGISTRY" --eval-db "$EVAL_DB" --report-dir "$OUT_DIR" \ + --host nas rc_batch=$? echo "=== $(date '+%F %T') stage1 monthly_batch exit=$rc_batch ===" if [ "$rc_batch" -ne 0 ]; then @@ -106,7 +107,8 @@ PY -w /app \ sanguo_vnpy_v2:lock-aligned \ -m sanguo_factor.data_gap_check \ - --registry "$REGISTRY" --manifest "$MANIFEST" --out-dir "$OUT_DIR" + --registry "$REGISTRY" --manifest "$MANIFEST" --out-dir "$OUT_DIR" \ + --host nas rc_gap=$? echo "=== $(date '+%F %T') stage3 data_gap_check exit=$rc_gap (2=缺口开issue,有效) ===" diff --git a/tests/factor/test_data_gap_check.py b/tests/factor/test_data_gap_check.py index debc711d..eb24dadd 100644 --- a/tests/factor/test_data_gap_check.py +++ b/tests/factor/test_data_gap_check.py @@ -301,3 +301,49 @@ def test_load_ledger_fps_tolerates_non_dict_json_line(tmp_path, capsys): encoding="utf-8") assert dgc.load_ledger_fps(str(p)) == {"abcd1234"} assert "非 dict" in capsys.readouterr().err + + +# —— P3-14: 开单权威单机制(host=vps 结构性拒开,关死双机同日竞态双开) —— +def test_maybe_open_host_vps_refuses_even_with_token(monkeypatch, tmp_path, + capsys): + import sanguo_factor.data_gap_check as dgc + monkeypatch.setenv("SANGUO_GITEA_TOKEN", "tok") + + def _boom(*a, **k): + raise AssertionError("host=vps 不得触网查询/开单") + monkeypatch.setattr(dgc, "find_open_issue", _boom) + monkeypatch.setattr(dgc, "open_gitea_issue", _boom) + assert dgc.maybe_open_gap_issue( + {"fa_x": ["s_new"]}, "m.yaml", + ledger_path=str(tmp_path / "ledger.jsonl"), host="vps") is None + assert "权威侧" in capsys.readouterr().err + + +def test_maybe_open_host_nas_keeps_current_behavior(monkeypatch, tmp_path): + import sanguo_factor.data_gap_check as dgc + monkeypatch.setenv("SANGUO_GITEA_TOKEN", "tok") + monkeypatch.setattr(dgc, "find_open_issue", lambda *a, **k: None) + opened = [] + monkeypatch.setattr(dgc, "open_gitea_issue", + lambda *a, **k: opened.append(a) or 7) + assert dgc.maybe_open_gap_issue( + {"fa_x": ["s_new"]}, "m.yaml", + ledger_path=str(tmp_path / "ledger.jsonl"), host="nas") == 7 + + +def test_main_host_flag_wires_into_maybe_open(tmp_path, monkeypatch): + _, reg_p = _reg(tmp_path) + mf = tmp_path / "manifest.yaml" + mf.write_text("domains: {pledge_detail: {desc: x}}\n", encoding="utf-8") + seen = {} + + def _fake(gaps, manifest_path, **kw): + seen.update(kw) + return None + import sanguo_factor.data_gap_check as dgc + monkeypatch.setattr(dgc, "maybe_open_gap_issue", _fake) + rc = dgc.main(["--registry", reg_p, "--manifest", str(mf), + "--out-dir", str(tmp_path / "out"), + "--gitea-open", "--host", "vps"]) + assert rc == 2 # 拒开不影响缺口信号 rc + assert seen.get("host") == "vps" diff --git a/tests/factor/test_monthly_batch.py b/tests/factor/test_monthly_batch.py index bd1afbc5..eab997f9 100644 --- a/tests/factor/test_monthly_batch.py +++ b/tests/factor/test_monthly_batch.py @@ -233,7 +233,7 @@ def test_main_reports_gaps_and_opens_issue(tmp_path, capsys, monkeypatch): "factors_done": 1}) opened = [] monkeypatch.setattr(dgc, "maybe_open_gap_issue", - lambda gaps, mp: opened.append(gaps) or 3) + lambda gaps, mp, **kw: opened.append(gaps) or 3) rc = mb.main(["--registry", str(reg), "--as-of", "2026-10-04", "--eval-db", str(tmp_path / "e.db"), "--manifest", str(mani), "--gap-issue",