"""端到端验证:因子参数化(暴露真实参数、可编辑、且真生效、真冻结)(2026-10)。 为什么单独一条契约脚本:因子参数写错**不会报错**,只会让策略按另一个口径选股 (「窗口 20」悄悄算成「窗口 90」= 另一个因子的结果)。所以这里从界面一路查到引擎: 1. `/factors`:目录暴露 template/params/param_specs(中文名含参数、允许范围 2~500、 方向二选一);展开行给出精确引擎键与来源;页面不再是「不可修改」的旧说法。 2. `/factors`:用界面新建参数化因子(窗口 90、越低越好)—— 预览键必须与后端规范键 逐字符一致;创建成功后 API 里出现该行,中文名/回看/方向全部由引擎投影。 3. `/factors`:越界参数(窗口 999)在界面上被拒,且**没有**留下任何垃圾行。 4. `/factors`:停用/启用真的写库,且停用后仍能被引擎解析(历史不变义)。 5. `/strategies`:条件字段下拉里出现该参数化因子(因子目录并入字段库), 因子下拉显示中文名(含参数)而不是又长又生的引擎键。 6. 375px:页面无横向溢出、几何检查全过。 7. 收尾:删掉验证期间创建的临时因子(目录没有删除接口,直接按主键清),回到 11 条内置。 需要浏览器可用的 Web(:3000)与 API(:8000)。复用 verify_ui_alignment 的 CDP 管线 (系统 Chrome,无需 playwright)。 """ from __future__ import annotations import json import os import subprocess import sys import tempfile import time import urllib.error import urllib.request sys.path.insert(0, "/Users/summer/project/qlib/scripts") from verify_ui_alignment import CDP, check_geometry, find_chrome, free_port, probe # noqa: E402 WEB = "http://127.0.0.1:3000" API = "http://127.0.0.1:8000" TMP_KEY = "momentum(window=90,direction=lower_is_better)" BAD_KEY = "momentum(window=999,direction=lower_is_better)" FACTORS_READ = r""" (() => { const t = (el) => (el ? el.innerText.replace(/\s+/g, " ").trim() : null); const rows = [...document.querySelectorAll("table.tbl tbody tr")]; const find = (name) => rows.find((r) => (r.innerText || "").includes(name)); const mv = find("momentum_60"); const mvCells = mv ? [...mv.querySelectorAll("td")].map((td) => t(td)) : []; const probe = find("__PROBE__"); const probeCells = probe ? [...probe.querySelectorAll("td")].map((td) => t(td)) : []; const probeBtn = probe ? [...probe.querySelectorAll("button")].map((b) => t(b)) : []; return JSON.stringify({ rowCount: rows.length, headers: [...document.querySelectorAll("table.tbl thead th")].map((th) => t(th)), momentumCells: mvCells, probeCells, probeButtons: probeBtn, // 展开行的文本:主行的下一个兄弟 tr(class=expand-cell)就是展开内容 probeExpandedText: probe ? t(probe.nextElementSibling) : null, bodyText: t(document.body), }); })() """ PREVIEW_READ = r""" (() => { const t = (el) => (el ? el.innerText.replace(/\s+/g, " ").trim() : null); const w = document.querySelector("#factor-new-window"); const d = document.querySelector("#factor-new-direction"); const all = t(document.body) || ""; // 预览键 = 模板名(参数=值,…)。**不要**扫整页文本: // 页面说明里也有一个示例键,扫文本会拿示例当预览(假通过)。 const previewKeys = [ ...new Set( [...document.querySelectorAll("b.mono")] .map((el) => t(el)) .filter((x) => x && /^[a-z_][a-z0-9_]*\(/.test(x)), ), ]; return JSON.stringify({ windowValue: w ? w.value : null, windowMin: w ? w.min : null, windowMax: w ? w.max : null, directionOptions: d ? [...d.options].map((o) => o.textContent.trim()) : [], directionValue: d ? d.value : null, templateOptions: [...(document.querySelectorAll("#factor-new-template option") || [])] .map((o) => o.textContent.trim()), previewKeys, bodyText: all, }); })() """ STRATEGY_FORM_READ = r""" (() => { const t = (el) => (el ? el.innerText.replace(/\s+/g, " ").trim() : null); const factorSel = document.querySelector('select[aria-label="第 1 个因子的名称"]'); const condSel = document.querySelector('select[aria-label="第 1 个条件的字段"]'); // 注意: